diff --git a/azext_iot/__init__.py b/azext_iot/__init__.py
index b7b439a25..f44bb927a 100644
--- a/azext_iot/__init__.py
+++ b/azext_iot/__init__.py
@@ -20,8 +20,17 @@
client_factory=iot_service_provisioning_factory
)
+iotdigitaltwin_ops = CliCommandType(
+ operations_tmpl='azext_iot.operations.digitaltwin#{}'
+)
+
+iotpnp_ops = CliCommandType(
+ operations_tmpl='azext_iot.operations.pnp#{}'
+)
+
class IoTExtCommandsLoader(AzCommandsLoader):
+
def __init__(self, cli_ctx=None):
super(IoTExtCommandsLoader, self).__init__(cli_ctx=cli_ctx)
diff --git a/azext_iot/_constants.py b/azext_iot/_constants.py
index 98e7689ca..afd8c90d5 100644
--- a/azext_iot/_constants.py
+++ b/azext_iot/_constants.py
@@ -6,7 +6,7 @@
import os
-VERSION = '0.7.1'
+VERSION = '0.8.0'
EXTENSION_NAME = 'azure-cli-iot-ext'
EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__))
EXTENSION_CONFIG_ROOT_KEY = 'iotext'
@@ -16,6 +16,9 @@
MIN_SIM_MSG_INTERVAL = 1
MIN_SIM_MSG_COUNT = 1
SIM_RECEIVE_SLEEP_SEC = 3
+PNP_API_VERSION = '2019-07-01-preview'
+PNP_ENDPOINT = 'https://provider.azureiotrepository.com'
+PNP_REPO_ENDPOINT = 'https://repo.azureiotrepository.com'
DEVICE_DEVICESCOPE_PREFIX = 'ms-azure-iot-edge://'
TRACING_PROPERTY = 'azureiot*com^dtracing^1'
TRACING_ALLOWED_FOR_LOCATION = ('northeurope', 'westus2', 'west us 2', 'southeastasia')
diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py
index 0500ba5e3..2f8f8f2fe 100644
--- a/azext_iot/_factory.py
+++ b/azext_iot/_factory.py
@@ -55,6 +55,7 @@ def _bind_sdk(target, sdk_type, device_id=None):
from azext_iot.custom_sdk.custom_api import CustomClient
from azext_iot.dps_sdk import ProvisioningServiceClient
+ from azext_iot.pnp_sdk.digital_twin_repository_service import DigitalTwinRepositoryService
sas_uri = target['entity']
endpoint = "https://{}".format(sas_uri)
@@ -97,6 +98,12 @@ def _bind_sdk(target, sdk_type, device_id=None):
_get_sdk_exception_type(sdk_type)
)
+ if sdk_type is SdkType.pnp_sdk:
+ return (
+ DigitalTwinRepositoryService(endpoint),
+ _get_sdk_exception_type(sdk_type)
+ )
+
return None
@@ -107,6 +114,7 @@ def _get_sdk_exception_type(sdk_type):
SdkType.custom_sdk: import_module('azext_iot.custom_sdk.models.error_details'),
SdkType.service_sdk: import_module('msrestazure.azure_exceptions'),
SdkType.device_sdk: import_module('msrestazure.azure_exceptions'),
- SdkType.dps_sdk: import_module('azext_iot.dps_sdk.models.provisioning_service_error_details')
+ SdkType.dps_sdk: import_module('azext_iot.dps_sdk.models.provisioning_service_error_details'),
+ SdkType.pnp_sdk: import_module('msrest.exceptions')
}
return exception_library.get(sdk_type, None)
diff --git a/azext_iot/_help.py b/azext_iot/_help.py
index 068d668b4..4ff15b9ab 100644
--- a/azext_iot/_help.py
+++ b/azext_iot/_help.py
@@ -912,3 +912,253 @@
type: command
short-summary: Delete a device registration in an Azure IoT Hub Device Provisioning Service.
"""
+
+helps['iot dt'] = """
+ type: group
+ short-summary: Manage digital twin of an IoT Plug and Play device.
+"""
+
+helps['iot dt invoke-command'] = """
+ type: command
+ short-summary: Executes a command on an IoT Plug and Play device.
+ long-summary: You can leverage az login and provide --hub-name instead of --login for every command.
+ examples:
+ - name: Execute a command on device .
+ text: >
+ az iot dt invoke-command --login {iothub_cs}
+ --interface {plug_and_play_interface} --device-id {device_id}
+ --command-name {command_name} --command-payload {payload}
+ - name: Execute a command on device within current session.
+ text: >
+ az iot dt invoke-command --hub-name {iothub_name}
+ --interface {plug_and_play_interface} --device-id {device_id}
+ --command-name {command_name} --command-payload {payload}
+"""
+
+helps['iot dt list-interfaces'] = """
+ type: command
+ short-summary: List interfaces of a target IoT Plug and Play device.
+ long-summary: You can leverage az login and provide --hub-name instead of --login for every command.
+ examples:
+ - name: List all IoT Plug and Play interfaces on a device.
+ text: >
+ az iot dt list-interfaces --login {iothub_cs}
+ --device-id {device_id}
+ - name: List all IoT Plug and Play interfaces on a device within current session.
+ text: >
+ az iot dt list-interfaces --hub-name {iothub_name} --device-id {device_id}
+"""
+
+helps['iot dt list-properties'] = """
+ type: command
+ short-summary: List properties of a target IoT Plug and Play device interface(s).
+ long-summary: You can leverage az login and provide --hub-name instead of --login for every command.
+ examples:
+ - name: List all properties of all device's interfaces on an IoT Plug and Play device.
+ text: >
+ az iot dt list-properties --login {iothub_cs} --source device
+ --device-id {device_id}
+ - name: List all properties of all public interfaces on an IoT Plug and Play device within current session.
+ text: >
+ az iot dt list-properties --hub-name {iothub_name} --device-id {device_id} --source public
+ - name: List all properties of device's interface on an IoT Plug and Play device.
+ text: >
+ az iot dt list-properties --login {iothub_cs} --source device
+ --device-id {device_id} --interface {plug_and_play_interface}
+"""
+
+helps['iot dt list-commands'] = """
+ type: command
+ short-summary: List commands of an IoT Plug and Play devices interface(s).
+ long-summary: You can leverage az login and provide --hub-name instead of --login for every command.
+ examples:
+ - name: List all commands of all private interfaces on an IoT Plug and Play device.
+ text: >
+ az iot dt list-commands --login {iothub_cs} --source private
+ --device-id {device_id} --repo-id {plug_and_play_model_repository_id}
+ - name: List all commands of a private interface on an IoT Plug and Play device.
+ text: >
+ az iot dt list-commands --login {iothub_cs} --source private
+ --device-id {device_id} --repo-id {plug_and_play_model_repository_id}
+ --interface {plug_and_play_interface}
+ - name: List all commands of all public interfaces on an IoT Plug and Play device.
+ text: >
+ az iot dt list-commands --login {iothub_cs} --source public
+ --device-id {device_id}
+ - name: List all commands of device's interface on an IoT Plug and Play device.
+ text: >
+ az iot dt list-commands --login {iothub_cs} --source device
+ --device-id {device_id} --interface {plug_and_play_interface}
+"""
+
+helps['iot dt monitor-events'] = """
+ type: command
+ short-summary: Monitor digital twin events.
+ long-summary: You can leverage az login and provide --hub-name instead of --login for every command.
+ examples:
+ - name: Monitor digital twin events of device's interface.
+ text: >
+ az iot dt monitor-events --login {iothub_cs} --device-id {device_id} --source device
+ --interface {plug_and_play_interface} --consumer-group {consumer_group_name}
+ - name: Monitor digital twin events of public interface within current session.
+ text: >
+ az iot dt monitor-events --hub-name {iothub_name} --device-id {device_id} --source public
+ --interface {plug_and_play_interface} --consumer-group {consumer_group_name}
+ - name: Monitor digital twin events of device's interface and see all message properties.
+ text: >
+ az iot dt monitor-events --login {iothub_cs} --device-id {device_id}
+ --interface {plug_and_play_interface} --consumer-group {consumer_group_name}
+ --properties all --source device
+"""
+
+helps['iot dt update-property'] = """
+ type: command
+ short-summary: Update an IoT Plug and Play device interfaces writable property.
+ examples:
+ - name: Update an IoT Plug and Play device interfaces read-write property.
+ text: >
+ az iot dt update-property --login {iothub_cs} --device-id {device_id}
+ --interface-payload {payload}
+ - name: Update an IoT Plug and Play device interfaces read-write property within current session.
+ text: >
+ az iot dt update-property --hub-name {iothub_name} --device-id {device_id}
+ --interface-payload {payload}
+"""
+
+helps['iot pnp'] = """
+ type: group
+ short-summary: Manage entities of an IoT Plug and Play model repository.
+"""
+
+helps['iot pnp interface'] = """
+ type: group
+ short-summary: Manage interfaces in an IoT Plug and Play model repository.
+"""
+
+helps['iot pnp interface publish'] = """
+ type: command
+ short-summary: Publish an interface to public repository.
+ examples:
+ - name: Publish an interface to public repository.
+ text: >
+ az iot pnp interface publish -r {pnp_repository} --interface {plug_and_play_interface_id}
+"""
+
+helps['iot pnp interface create'] = """
+ type: command
+ short-summary: Create an interface in the company repository.
+ examples:
+ - name: Create an interface in the company repository.
+ text: >
+ az iot pnp interface create --def {plug_and_play_interface_file_path} -r {pnp_repository}
+"""
+
+helps['iot pnp interface update'] = """
+ type: command
+ short-summary: Update an interface in the company repository.
+ examples:
+ - name: Update an interface in the company repository.
+ text: >
+ az iot pnp interface update --def {updated_plug_and_play_interface_file_path} -r {pnp_repository}
+"""
+
+helps['iot pnp interface list'] = """
+ type: command
+ short-summary: List all interfaces.
+ examples:
+ - name: List all company repository's interfaces.
+ text: >
+ az iot pnp interface list -r {pnp_repository}
+ - name: List all public interfaces.
+ text: >
+ az iot pnp interface list
+"""
+
+helps['iot pnp interface show'] = """
+ type: command
+ short-summary: Get the details of an interface.
+ examples:
+ - name: Get the details of a company repository interface.
+ text: >
+ az iot pnp interface show -r {pnp_repository} --interface {plug_and_play_interface_id}
+ - name: Get the details of public interface.
+ text: >
+ az iot pnp interface show --interface {plug_and_play_interface_id}
+"""
+
+helps['iot pnp interface delete'] = """
+ type: command
+ short-summary: Delete an interface in the company repository.
+ examples:
+ - name: Delete an interface in the company repository.
+ text: >
+ az iot pnp interface delete -r {pnp_repository} --interface {plug_and_play_interface_id}
+"""
+
+helps['iot pnp capability-model'] = """
+ type: group
+ short-summary: Manage device capability models in an IoT Plug and Play model repository.
+"""
+
+helps['iot pnp capability-model list'] = """
+ type: command
+ short-summary: List all capability-model.
+ examples:
+ - name: List all company repository's capability-model.
+ text: >
+ az iot pnp capability-model list -r {pnp_repository}
+ - name: List all public capability-model.
+ text: >
+ az iot pnp capability-model list
+"""
+
+helps['iot pnp capability-model show'] = """
+ type: command
+ short-summary: Get the details of a capability-model.
+ examples:
+ - name: Get the details of a company repository capability-model.
+ text: >
+ az iot pnp capability-model show -r {pnp_repository} --model {plug_and_play_capability_model_id}
+ - name: Get the details of public capability-model.
+ text: >
+ az iot pnp capability-model show --model {plug_and_play_capability_model_id}
+"""
+
+helps['iot pnp capability-model create'] = """
+ type: command
+ short-summary: Create a capability-model in the company repository.
+ examples:
+ - name: Create a capability-model in the company repository.
+ text: >
+ az iot pnp capability-model create --def {plug_and_play_capability_model_file_path} -r {pnp_repository}
+"""
+
+helps['iot pnp capability-model publish'] = """
+ type: command
+ short-summary: Publish the capability-model to public repository.
+ examples:
+ - name: Publish the capability-model to public repository.
+ text: >
+ az iot pnp capability-model publish -r {pnp_repository}
+ --model {plug_and_play_capability_model_id}
+"""
+
+helps['iot pnp capability-model delete'] = """
+ type: command
+ short-summary: Delete the capability-model in the company repository.
+ examples:
+ - name: Delete the capability-model in the company repository.
+ text: >
+ az iot pnp capability-model delete -r {pnp_repository}
+ --model {plug_and_play_capability_model_id}
+"""
+
+helps['iot pnp capability-model update'] = """
+ type: command
+ short-summary: Update the capability-model in the company repository.
+ examples:
+ - name: Update the capability-model in the company repository.
+ text: >
+ az iot pnp capability-model update --def {updated_plug_and_play_capability_model_file_path}
+ -r {pnp_repository}
+"""
diff --git a/azext_iot/_params.py b/azext_iot/_params.py
index dcee8d2ff..10c96ff75 100644
--- a/azext_iot/_params.py
+++ b/azext_iot/_params.py
@@ -26,7 +26,8 @@
MetricType,
ReprovisionType,
AllocationType,
- DistributedTracingSamplingModeType
+ DistributedTracingSamplingModeType,
+ ModelSourceType
)
from azext_iot._validators import mode2_iot_login_handler
@@ -87,6 +88,8 @@ def load_arguments(self, _):
context.argument('repair', options_list=['--repair', '-r'],
arg_type=get_three_state_flag(),
help='Reinstall uamqp dependency compatible with extension version. Default: false')
+ context.argument('repo_endpoint', options_list=['--endpoint', '-e'], help='IoT Plug and Play endpoint.')
+ context.argument('repo_id', options_list=['--repo-id', '-r'], help='IoT Plug and Play repository Id.')
with self.argument_context('iot hub') as context:
context.argument('target_json', options_list=['--json', '-j'],
@@ -378,3 +381,70 @@ def load_arguments(self, _):
with self.argument_context('iot dps registration list') as context:
context.argument('enrollment_id', help='ID of enrollment group')
+
+ with self.argument_context('iot dt') as context:
+ context.argument('repo_login', options_list=['--repo-login', '--rl'],
+ help='This command supports an entity connection string with rights to perform action. '
+ 'Use to avoid PnP endpoint and repository name if repository is private. '
+ 'If both an entity connection string and name are provided the connection string takes priority.')
+ context.argument('interface', options_list=['--interface', '-i'],
+ help='Target interface name. This should be the name of the interface not the urn-id.')
+ context.argument('command_name', options_list=['--command-name', '--cn'],
+ help='IoT Plug and Play interface command name.')
+ context.argument('command_payload', options_list=['--command-payload', '--cp', '--cv'],
+ help='IoT Plug and Play interface command payload. '
+ 'Content can be directly input or extracted from a file path.')
+ context.argument('interface_payload', options_list=['--interface-payload', '--ip', '--iv'],
+ help='IoT Plug and Play interface payload. '
+ 'Content can be directly input or extracted from a file path.')
+ context.argument('source_model', options_list=['--source', '-s'],
+ help='Choose your option to get model definition from specified source. ',
+ arg_type=get_enum_type(ModelSourceType))
+ context.argument('schema', options_list=['--schema'],
+ help='Show interface with entity schema.')
+
+ with self.argument_context('iot dt monitor-events') as context:
+ context.argument('consumer_group', options_list=['--consumer-group', '--cg'],
+ help='Specify the consumer group to use when connecting to event hub endpoint.')
+ context.argument('properties', options_list=['--properties', '--props', '-p'], arg_type=event_msg_prop_type)
+ context.argument('pnp_context', options_list=['--pnp-context'],
+ arg_type=get_three_state_flag(),
+ help='Plug and Play telemetry context.')
+ context.argument('repair', options_list=['--repair'],
+ arg_type=get_three_state_flag(),
+ help='Reinstall uamqp dependency compatible with extension version. Default: false')
+
+ with self.argument_context('iot pnp') as context:
+ context.argument('model', options_list=['--model', '-m'],
+ help='Target capability-model urn-id. Example: urn:example:capabilityModels:Mxchip:1')
+ context.argument('interface', options_list=['--interface', '-i'],
+ help='Target interface urn-id. Example: urn:example:interfaces:MXChip:1')
+
+ with self.argument_context('iot pnp interface') as context:
+ context.argument('interface_definition', options_list=['--definition', '--def'],
+ help='IoT Plug and Play interface definition written in PPDL (JSON-LD). '
+ 'Can be directly input or a file path where the content is extracted.')
+
+ with self.argument_context('iot pnp interface list') as context:
+ context.argument('search_string', options_list=['--search', '--ss'],
+ help='Searches IoT Plug and Play interfaces for given string in the'
+ ' \"Description, DisplayName, comment and Id\".')
+ context.argument('top', type=int, options_list=['--top'],
+ help='Maximum number of interface to return.')
+
+ with self.argument_context('iot pnp capability-model') as context:
+ context.argument('model_definition', options_list=['--definition', '--def'],
+ help='IoT Plug and Play capability-model definition written in PPDL (JSON-LD). '
+ 'Can be directly input or a file path where the content is extracted.')
+
+ with self.argument_context('iot pnp capability-model show') as context:
+ context.argument('expand', options_list=['--expand'],
+ help='Indicates whether to expand the device capability model\'s'
+ ' interface definitions or not.')
+
+ with self.argument_context('iot pnp capability-model list') as context:
+ context.argument('search_string', options_list=['--search', '--ss'],
+ help='Searches IoT Plug and Play models for given string in the'
+ ' \"Description, DisplayName, comment and Id\".')
+ context.argument('top', type=int, options_list=['--top'],
+ help='Maximum number of capability-model to return.')
diff --git a/azext_iot/_validators.py b/azext_iot/_validators.py
index 4cdb3355e..95cb23acf 100644
--- a/azext_iot/_validators.py
+++ b/azext_iot/_validators.py
@@ -22,6 +22,9 @@ def mode2_iot_login_handler(cmd, namespace):
elif 'dps_name' in arg_keys:
iot_cmd_type = 'DPS'
entity_value = args.get('dps_name')
+ elif 'repo_endpoint' in arg_keys:
+ iot_cmd_type = 'PnP'
+ entity_value = args.get('repo_endpoint')
if not any([login_value, entity_value]):
raise CLIError(ERROR_NO_HUB_OR_LOGIN_ON_INPUT(iot_cmd_type))
diff --git a/azext_iot/azext_metadata.json b/azext_iot/azext_metadata.json
index 0fa9ed9de..90c1960c4 100644
--- a/azext_iot/azext_metadata.json
+++ b/azext_iot/azext_metadata.json
@@ -1,4 +1,4 @@
{
"azext.minCliCoreVersion": "2.0.24",
- "version": "0.7.1"
+ "version": "0.8.0"
}
\ No newline at end of file
diff --git a/azext_iot/commands.py b/azext_iot/commands.py
index 32dc43fbf..0ab883b9b 100644
--- a/azext_iot/commands.py
+++ b/azext_iot/commands.py
@@ -9,7 +9,7 @@
Load CLI commands
"""
-from azext_iot import iothub_ops, iotdps_ops
+from azext_iot import iothub_ops, iotdps_ops, iotdigitaltwin_ops, iotpnp_ops
def load_command_table(self, _):
@@ -120,3 +120,27 @@ def load_command_table(self, _):
cmd_group.command('list', 'iot_dps_registration_list')
cmd_group.command('show', 'iot_dps_registration_get')
cmd_group.command('delete', 'iot_dps_registration_delete')
+
+ with self.command_group('iot dt', command_type=iotdigitaltwin_ops) as cmd_group:
+ cmd_group.command('list-interfaces', 'iot_digitaltwin_interface_list')
+ cmd_group.command('list-properties', 'iot_digitaltwin_properties_list')
+ cmd_group.command('update-property', 'iot_digitaltwin_property_update')
+ cmd_group.command('invoke-command', 'iot_digitaltwin_invoke_command')
+ cmd_group.command('monitor-events', 'iot_digitaltwin_monitor_events')
+ cmd_group.command('list-commands', 'iot_digitaltwin_command_list')
+
+ with self.command_group('iot pnp interface', command_type=iotpnp_ops) as cmd_group:
+ cmd_group.command('show', 'iot_pnp_interface_show')
+ cmd_group.command('list', 'iot_pnp_interface_list')
+ cmd_group.command('create', 'iot_pnp_interface_create')
+ cmd_group.command('publish', 'iot_pnp_interface_publish')
+ cmd_group.command('delete', 'iot_pnp_interface_delete')
+ cmd_group.command('update', 'iot_pnp_interface_update')
+
+ with self.command_group('iot pnp capability-model', command_type=iotpnp_ops) as cmd_group:
+ cmd_group.command('show', 'iot_pnp_model_show')
+ cmd_group.command('list', 'iot_pnp_model_list')
+ cmd_group.command('create', 'iot_pnp_model_create')
+ cmd_group.command('publish', 'iot_pnp_model_publish')
+ cmd_group.command('delete', 'iot_pnp_model_delete')
+ cmd_group.command('update', 'iot_pnp_model_update')
diff --git a/azext_iot/common/_azure.py b/azext_iot/common/_azure.py
index da6308de9..ae58b831a 100644
--- a/azext_iot/common/_azure.py
+++ b/azext_iot/common/_azure.py
@@ -21,6 +21,11 @@ def _parse_connection_string(cs, validate=None, cstring_type='entity'):
return decomposed
+def parse_pnp_connection_string(cs):
+ validate = ['HostName', 'RepositoryId', 'SharedAccessKeyName', 'SharedAccessKey']
+ return _parse_connection_string(cs, validate, 'PnP Model Repository')
+
+
def parse_iot_hub_connection_string(cs):
validate = ['HostName', 'SharedAccessKeyName', 'SharedAccessKey']
return _parse_connection_string(cs, validate, 'IoT Hub')
@@ -217,3 +222,86 @@ def _find_iot_dps_from_list(all_dps, dps_name):
result['subscription'] = client.config.subscription_id
return result
+
+
+# pylint: disable=broad-except
+def get_iot_pnp_connection_string(
+ cmd,
+ endpoint,
+ repo_id,
+ user_role='Admin',
+ login=None):
+ """
+ Function used to build up dictionary of IoT PnP connection string parts
+
+ Args:
+ cmd (object): Knack cmd
+ endpoint (str): PnP endpoint
+ repository_id (str): PnP repository Id.
+ user_role (str): User role of the access key for the given PnP repository.
+
+ Returns:
+ (dict): of connection string elements.
+
+ Raises:
+ CLIError: on input validation failure.
+
+ """
+
+ # pylint: disable=line-too-long
+ from azure.cli.command_modules.iot.digitaltwinrepositoryprovisioningservice import DigitalTwinRepositoryProvisioningService
+ from azure.cli.command_modules.iot._utils import get_auth_header
+ from azext_iot._constants import PNP_REPO_ENDPOINT
+
+ result = {}
+ client = None
+ headers = None
+
+ if login:
+
+ try:
+ decomposed = parse_pnp_connection_string(login)
+ except ValueError as e:
+ raise CLIError(e)
+
+ result = {}
+ result['cs'] = login
+ result['policy'] = decomposed['SharedAccessKeyName']
+ result['primarykey'] = decomposed['SharedAccessKey']
+ result['repository_id'] = decomposed['RepositoryId']
+ result['entity'] = decomposed['HostName']
+ result['entity'] = result['entity'].replace('https://', '')
+ result['entity'] = result['entity'].replace('http://', '')
+ return result
+
+ def _find_key_from_list(keys, user_role):
+ if keys:
+ return next((key for key in keys if key.user_role.lower() == user_role.lower()), None)
+ return None
+
+ if repo_id:
+ client = DigitalTwinRepositoryProvisioningService(endpoint)
+ headers = get_auth_header(cmd)
+ keys = client.get_keys_async(repository_id=repo_id, api_version=client.api_version, custom_headers=headers)
+
+ if keys is None:
+ raise CLIError('Auth key required for repository "{}"'.format(repo_id))
+
+ policy = _find_key_from_list(keys, user_role)
+
+ if policy is None:
+ raise CLIError(
+ 'No auth key found for repository "{}" with user_role "{}".'.format(repo_id, user_role)
+ )
+
+ result['cs'] = policy.connection_string
+ result['entity'] = policy.service_endpoint
+ result['policy'] = policy.id
+ result['primarykey'] = policy.secret
+ result['repository_id'] = policy.repository_id
+ else:
+ result['entity'] = PNP_REPO_ENDPOINT
+
+ result['entity'] = result['entity'].replace('https://', '')
+ result['entity'] = result['entity'].replace('http://', '')
+ return result
diff --git a/azext_iot/common/digitaltwin_sas_token_auth.py b/azext_iot/common/digitaltwin_sas_token_auth.py
new file mode 100644
index 000000000..eb98a74ce
--- /dev/null
+++ b/azext_iot/common/digitaltwin_sas_token_auth.py
@@ -0,0 +1,66 @@
+# coding=utf-8
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for license information.
+# --------------------------------------------------------------------------------------------
+# pylint: disable=too-few-public-methods
+"""
+digitaltwin_sas_token_auth: Module containing DigitalTwin Model Shared Access Signature token class.
+
+"""
+
+from base64 import b64encode, b64decode
+from hashlib import sha256
+from hmac import HMAC
+from time import time
+try:
+ from urllib import (urlencode, quote_plus)
+except ImportError:
+ from urllib.parse import (urlencode, quote_plus) # pylint: disable=import-error
+from msrest.authentication import Authentication
+
+
+class DigitalTwinSasTokenAuthentication(Authentication):
+ """
+ Shared Access Signature authorization for DigitalTwin Repository.
+
+ Args:
+ uri (str): Uri of target resource.
+ shared_access_policy_name (str): Name of shared access policy.
+ shared_access_key (str): Shared access key.
+ expiry (int): Expiry of the token to be generated. Input should
+ be seconds since the epoch, in UTC. Default is an hour later from now.
+ """
+ def __init__(self, repositoryId, endpoint, shared_access_key_name, shared_access_key, expiry=None):
+ self.repositoryId = repositoryId
+ self.policy = shared_access_key_name
+ self.key = shared_access_key
+ self.endpoint = endpoint
+ if expiry is None:
+ self.expiry = time() + 3600 # Default expiry is an hour later
+ else:
+ self.expiry = expiry
+
+ def generate_sas_token(self):
+ """
+ Create a shared access signiture token as a string literal.
+
+ Returns:
+ result (str): SAS token as string literal.
+ """
+ encoded_uri = quote_plus(self.endpoint)
+ encoded_repo_id = quote_plus(self.repositoryId)
+ ttl = int(self.expiry)
+ sign_key = '%s\n%s\n%d' % (encoded_repo_id, encoded_uri, ttl)
+ signature = b64encode(HMAC(b64decode(self.key), sign_key.encode('utf-8'), sha256).digest())
+ result = {
+ 'sr': self.endpoint,
+ 'sig': signature,
+ 'se': str(ttl)
+ }
+
+ if self.policy:
+ result['skn'] = self.policy
+ result['rid'] = self.repositoryId
+
+ return 'SharedAccessSignature ' + urlencode(result)
diff --git a/azext_iot/common/shared.py b/azext_iot/common/shared.py
index 0de70ed61..15e9dce51 100644
--- a/azext_iot/common/shared.py
+++ b/azext_iot/common/shared.py
@@ -25,6 +25,7 @@ class SdkType(Enum):
dps_sdk = 5
device_sdk = 6
service_sdk = 7
+ pnp_sdk = 8
# pylint: disable=too-few-public-methods
@@ -139,3 +140,23 @@ class DistributedTracingSamplingModeType(Enum):
"""
off = 'off'
on = 'on'
+
+
+# pylint: disable=too-few-public-methods
+class PnPModelType(Enum):
+ """
+ Type of PnP Model.
+ """
+ any = 'any'
+ interface = 'Interface'
+ capabilityModel = 'capabilityModel'
+
+
+# pylint: disable=too-few-public-methods
+class ModelSourceType(Enum):
+ """
+ Type of source to get model definition.
+ """
+ public = 'public'
+ private = 'private'
+ device = 'device'
diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py
index a4f5985a5..cc4f4cc2c 100644
--- a/azext_iot/common/utility.py
+++ b/azext_iot/common/utility.py
@@ -259,15 +259,29 @@ def test_import(package):
return True
+def unpack_pnp_http_error(e):
+ error = unpack_msrest_error(e)
+ if isinstance(error, dict):
+ if error.get('error'):
+ error = error['error']
+ if error.get('stackTrace'):
+ error.pop('stackTrace')
+ return error
+
+
def unpack_msrest_error(e, clouderror=True):
""" Obtains full response text from an msrest error """
if clouderror:
+ op_err = None
try:
- return json.loads(e.response.text)
+ op_err = json.loads(e.response.text)
except ValueError:
- return e.response.text
+ op_err = e.response.text
except TypeError:
- return e.response.text
+ op_err = e.response.text
+ if not op_err:
+ return str(e)
+ return op_err
return e
@@ -280,3 +294,21 @@ def calculate_millisec_since_unix_epoch_utc():
now = datetime.utcnow()
epoch = datetime.utcfromtimestamp(0)
return int(1000 * (now - epoch).total_seconds())
+
+
+def get_sas_token(target):
+ from azext_iot.common.digitaltwin_sas_token_auth import DigitalTwinSasTokenAuthentication
+ token = ''
+ if target.get('repository_id'):
+ token = DigitalTwinSasTokenAuthentication(target["repository_id"],
+ target["entity"],
+ target["policy"],
+ target["primarykey"]).generate_sas_token()
+ return {'Authorization': '{}'.format(token)}
+
+
+def dict_clean(d):
+ """ Remove None from dictionary """
+ if not isinstance(d, dict):
+ return d
+ return dict((k, dict_clean(v)) for k, v in d.items() if v is not None)
diff --git a/azext_iot/operations/_mqtt.py b/azext_iot/operations/_mqtt.py
index 1785446d7..9d36d62a6 100644
--- a/azext_iot/operations/_mqtt.py
+++ b/azext_iot/operations/_mqtt.py
@@ -8,9 +8,11 @@
import ssl
import os
-from time import time, sleep
import six
+
+from time import time, sleep
from paho.mqtt import client as mqtt
+
from azext_iot._constants import EXTENSION_ROOT
from azext_iot.common.sas_token_auth import SasTokenAuthentication
diff --git a/azext_iot/operations/digitaltwin.py b/azext_iot/operations/digitaltwin.py
new file mode 100644
index 000000000..d351f64f4
--- /dev/null
+++ b/azext_iot/operations/digitaltwin.py
@@ -0,0 +1,278 @@
+# coding=utf-8
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Unpublished works.
+# --------------------------------------------------------------------------------------------
+
+from os.path import exists
+from knack.util import CLIError
+from azure.cli.core.util import read_file_content
+from azext_iot._constants import PNP_ENDPOINT
+from azext_iot._factory import _bind_sdk
+from azext_iot.common.shared import SdkType, ModelSourceType
+from azext_iot.common._azure import get_iot_hub_connection_string
+from azext_iot.common.utility import (shell_safe_json_parse,
+ unpack_msrest_error)
+from azext_iot.operations.pnp import (iot_pnp_interface_show,
+ iot_pnp_interface_list,
+ _validate_repository)
+from azext_iot.operations.hub import _iot_hub_monitor_events
+
+
+INTERFACE_KEY_NAME = 'urn_azureiot_ModelDiscovery_DigitalTwin'
+INTERFACE_COMMAND = 'Command'
+INTERFACE_PROPERTY = 'Property'
+INTERFACE_TELEMETRY = 'Telemetry'
+INTERFACE_MODELDEFINITION = 'urn_azureiot_ModelDiscovery_ModelDefinition'
+INTERFACE_COMMANDNAME = 'getModelDefinition'
+
+
+def iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_group_name=None, login=None):
+ device_default_interface = _iot_digitaltwin_interface_show(cmd, device_id, INTERFACE_KEY_NAME,
+ hub_name, resource_group_name, login)
+ result = _get_device_default_interface_dict(device_default_interface)
+ return {'interfaces': result}
+
+
+def iot_digitaltwin_command_list(cmd, device_id, source_model, interface=None, schema=False,
+ repo_endpoint=PNP_ENDPOINT, repo_id=None, repo_login=None,
+ hub_name=None, resource_group_name=None, login=None):
+ result = []
+ target_interfaces = []
+ source_model = source_model.lower()
+ device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login)
+ interface_list = _get_device_default_interface_dict(device_interfaces)
+ target_interface = next((item for item in interface_list if item['name'] == interface), None)
+ if interface and not target_interface:
+ raise CLIError('Target interface is not implemented by the device!')
+
+ if interface:
+ target_interfaces.append(target_interface)
+ else:
+ target_interfaces = interface_list
+
+ for entity in target_interfaces:
+ interface_result = {'name': entity['name'], 'urn_id': entity['urn_id'], 'commands': {}}
+ interface_commands = []
+ found_commands = []
+ if source_model == ModelSourceType.device.value.lower():
+ found_commands = _device_interface_elements(cmd, device_id, entity['urn_id'], INTERFACE_COMMAND,
+ hub_name, resource_group_name, login)
+ else:
+ if source_model == ModelSourceType.private.value.lower():
+ _validate_repository(repo_id, repo_login)
+ found_commands = _pnp_interface_elements(cmd, entity['urn_id'], INTERFACE_COMMAND,
+ repo_endpoint, repo_id, repo_login)
+ for command in found_commands:
+ command.pop('@type', None)
+ if schema:
+ interface_commands.append(command)
+ else:
+ interface_commands.append(command.get('name'))
+ interface_result['commands'] = interface_commands
+ result.append(interface_result)
+ return {'interfaces': result}
+
+
+def iot_digitaltwin_properties_list(cmd, device_id, source_model, interface=None, schema=False,
+ repo_endpoint=PNP_ENDPOINT, repo_id=None, repo_login=None,
+ hub_name=None, resource_group_name=None, login=None):
+ result = []
+ target_interfaces = []
+ source_model = source_model.lower()
+ device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login)
+ interface_list = _get_device_default_interface_dict(device_interfaces)
+ target_interface = next((item for item in interface_list if item['name'] == interface), None)
+ if interface and not target_interface:
+ raise CLIError('Target interface is not implemented by the device!')
+
+ if interface:
+ target_interfaces.append(target_interface)
+ else:
+ target_interfaces = interface_list
+
+ for entity in target_interfaces:
+ interface_result = {'name': entity['name'], 'urn_id': entity['urn_id'], 'properties': {}}
+ interface_properties = []
+ found_properties = []
+ if source_model == ModelSourceType.device.value.lower():
+ found_properties = _device_interface_elements(cmd, device_id, entity['urn_id'], INTERFACE_PROPERTY,
+ hub_name, resource_group_name, login)
+ else:
+ if source_model == ModelSourceType.private.value.lower():
+ _validate_repository(repo_id, repo_login)
+ found_properties = _pnp_interface_elements(cmd, entity['urn_id'], INTERFACE_PROPERTY,
+ repo_endpoint, repo_id, repo_login)
+ for prop in found_properties:
+ prop.pop('@type', None)
+ if schema:
+ interface_properties.append(prop)
+ else:
+ interface_properties.append(prop.get('name'))
+ interface_result['properties'] = interface_properties
+ result.append(interface_result)
+ return {'interfaces': result}
+
+
+# pylint: disable=too-many-locals
+def iot_digitaltwin_invoke_command(cmd, interface, device_id, command_name, command_payload=None,
+ timeout=10, hub_name=None, resource_group_name=None, login=None):
+ device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login)
+ interface_list = _get_device_default_interface_dict(device_interfaces)
+
+ target_interface = next((item for item in interface_list if item['name'] == interface), None)
+
+ if not target_interface:
+ raise CLIError('Target interface is not implemented by the device!')
+
+ if command_payload:
+ if exists(command_payload):
+ command_payload = str(read_file_content(command_payload))
+
+ target_json = None
+ try:
+ target_json = shell_safe_json_parse(command_payload)
+ except ValueError:
+ pass
+
+ if target_json or isinstance(target_json, bool):
+ command_payload = target_json
+
+ target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
+ service_sdk, errors = _bind_sdk(target, SdkType.service_sdk)
+ try:
+ result = service_sdk.invoke_interface_command(device_id,
+ interface,
+ command_name,
+ command_payload,
+ connect_timeout_in_seconds=timeout,
+ response_timeout_in_seconds=timeout)
+ return result
+ except errors.CloudError as e:
+ raise CLIError(unpack_msrest_error(e))
+
+
+def iot_digitaltwin_property_update(cmd, interface_payload, device_id,
+ hub_name=None, resource_group_name=None, login=None):
+ if exists(interface_payload):
+ interface_payload = str(read_file_content(interface_payload))
+
+ target_json = None
+ try:
+ target_json = shell_safe_json_parse(interface_payload)
+ except ValueError:
+ pass
+
+ if target_json:
+ interface_payload = target_json
+
+ target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
+ service_sdk, errors = _bind_sdk(target, SdkType.service_sdk)
+ try:
+ result = service_sdk.update_interfaces(device_id, interfaces=interface_payload)
+ return result
+ except errors.CloudError as e:
+ raise CLIError(unpack_msrest_error(e))
+
+
+def iot_digitaltwin_monitor_events(cmd, device_id, interface, source_model, repo_endpoint=PNP_ENDPOINT, repo_id=None,
+ consumer_group='$Default', timeout=300, hub_name=None, resource_group_name=None,
+ yes=False, properties=None, repair=False, login=None, repo_login=None):
+ source_model = source_model.lower()
+ pnp_context = {'enabled': True, 'interface': {}}
+ device_interfaces = _iot_digitaltwin_interface_list(cmd, device_id, hub_name, resource_group_name, login)
+ interface_list = _get_device_default_interface_dict(device_interfaces)
+
+ target_interface = next((k for k in interface_list if k['name'] == interface), None)
+ if interface and not target_interface:
+ raise CLIError('Target interface is not implemented by the device!')
+
+ pnp_context['interface'][target_interface['urn_id']] = {}
+ found_telemetry = []
+ if source_model == ModelSourceType.device.value.lower():
+ found_telemetry = _device_interface_elements(cmd, device_id, target_interface['urn_id'], INTERFACE_TELEMETRY,
+ hub_name, resource_group_name, login)
+ else:
+ if source_model == ModelSourceType.private.value.lower():
+ _validate_repository(repo_id, repo_login)
+ found_telemetry = _pnp_interface_elements(cmd, target_interface['urn_id'], INTERFACE_TELEMETRY,
+ repo_endpoint, repo_id, repo_login)
+
+ for telemetry in found_telemetry:
+ telemetry_data = {'display': telemetry.get('displayName'), 'unit': telemetry.get('unit')}
+ pnp_context['interface'][target_interface['urn_id']][telemetry['name']] = telemetry_data
+
+ _iot_hub_monitor_events(cmd=cmd, interface=target_interface['urn_id'], pnp_context=pnp_context,
+ hub_name=hub_name, device_id=device_id, consumer_group=consumer_group, timeout=timeout,
+ enqueued_time=None, resource_group_name=resource_group_name,
+ yes=yes, properties=properties, repair=repair,
+ login=login)
+
+
+def _iot_digitaltwin_interface_show(cmd, device_id, interface, hub_name=None, resource_group_name=None, login=None):
+ target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
+ service_sdk, errors = _bind_sdk(target, SdkType.service_sdk)
+ try:
+ device_interface = service_sdk.get_interface(device_id, interface)
+ return device_interface
+ except errors.CloudError as e:
+ raise CLIError(unpack_msrest_error(e))
+
+
+def _iot_digitaltwin_interface_list(cmd, device_id, hub_name=None, resource_group_name=None, login=None):
+ target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
+ service_sdk, errors = _bind_sdk(target, SdkType.service_sdk)
+ try:
+ device_interfaces = service_sdk.get_interfaces(device_id)
+ return device_interfaces
+ except errors.CloudError as e:
+ raise CLIError(unpack_msrest_error(e))
+
+
+def _get_device_default_interface_dict(device_default_interface):
+ interface = device_default_interface['interfaces'][INTERFACE_KEY_NAME]
+ result = []
+ for k, v in interface['properties']['modelInformation']['reported']['value']['interfaces'].items():
+ result.append({'name': k, "urn_id": v})
+ return result
+
+
+def _pnp_interface_elements(cmd, interface, target_type, repo_endpoint, repo_id, login):
+ interface_elements = []
+ results = iot_pnp_interface_list(cmd, repo_endpoint, repo_id, interface, login=login)
+ if results:
+ interface_def = iot_pnp_interface_show(cmd, interface, repo_endpoint, repo_id, login)
+ interface_contents = interface_def.get('contents')
+ for content in interface_contents:
+ if isinstance(content.get('@type'), list) and target_type in content.get('@type'):
+ interface_elements.append(content)
+ elif content.get('@type') == target_type:
+ interface_elements.append(content)
+ return interface_elements
+
+
+def _device_interface_elements(cmd, device_id, interface, target_type, hub_name, resource_group_name, login):
+ target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
+ service_sdk, errors = _bind_sdk(target, SdkType.service_sdk)
+ interface_elements = []
+ try:
+ payload = {'id': {}}
+ payload['id'] = interface
+ target_payload = shell_safe_json_parse(str(payload))
+ interface_def = service_sdk.invoke_interface_command(device_id,
+ INTERFACE_MODELDEFINITION,
+ INTERFACE_COMMANDNAME,
+ target_payload)
+ if interface_def and interface_def.get('contents'):
+ interface_contents = interface_def.get('contents')
+ for content in interface_contents:
+ if isinstance(content.get('@type'), list) and target_type in content.get('@type'):
+ interface_elements.append(content)
+ elif content.get('@type') == target_type:
+ interface_elements.append(content)
+ return interface_elements
+ except errors.CloudError as e:
+ raise CLIError(unpack_msrest_error(e))
+ except Exception: # pylint: disable=broad-except
+ # returning an empty collection to continue
+ return []
diff --git a/azext_iot/operations/dps.py b/azext_iot/operations/dps.py
index 783a7b42b..0d800df5d 100644
--- a/azext_iot/operations/dps.py
+++ b/azext_iot/operations/dps.py
@@ -409,15 +409,16 @@ def iot_dps_registration_delete(client, dps_name, resource_group_name, registrat
def _get_initial_twin(initial_twin_tags=None, initial_twin_properties=None):
+ from azext_iot.common.utility import dict_clean
if initial_twin_tags == "":
initial_twin_tags = None
elif initial_twin_tags:
- initial_twin_tags = shell_safe_json_parse(str(initial_twin_tags))
+ initial_twin_tags = dict_clean(shell_safe_json_parse(str(initial_twin_tags)))
if initial_twin_properties == "":
initial_twin_properties = None
elif initial_twin_properties:
- initial_twin_properties = shell_safe_json_parse(str(initial_twin_properties))
+ initial_twin_properties = dict_clean(shell_safe_json_parse(str(initial_twin_properties)))
return InitialTwin(TwinCollection(initial_twin_tags),
InitialTwinProperties(TwinCollection(initial_twin_properties)))
diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py
index 0d93eae84..ccce57eea 100644
--- a/azext_iot/operations/events3/_events.py
+++ b/azext_iot/operations/events3/_events.py
@@ -10,11 +10,11 @@
import sys
from time import time
from uuid import uuid4
-
import six
import uamqp
import yaml
+
from azext_iot._constants import VERSION
from azext_iot.common.sas_token_auth import SasTokenAuthentication
from azext_iot.common.utility import (parse_entity, unicode_binary_map,
@@ -26,11 +26,15 @@
DEBUG = True
-def executor(target, consumer_group, enqueued_time, device_id=None, properties=None, timeout=0, output=None, content_type=None,
- devices=None):
+def executor(target, consumer_group, enqueued_time, properties=None,
+ timeout=0, device_id=None, output=None, content_type=None,
+ devices=None, interface_id=None, pnp_context=None):
+
coroutines = []
- coroutines.append(initiate_event_monitor(target, consumer_group, enqueued_time, device_id, properties,
- timeout, output, content_type, devices))
+ coroutines.append(initiate_event_monitor(target, consumer_group, enqueued_time,
+ device_id, properties, timeout, output, content_type, devices,
+ interface_id, pnp_context))
+
loop = asyncio.get_event_loop()
if loop.is_closed():
loop = asyncio.new_event_loop()
@@ -50,7 +54,8 @@ def stop_and_suppress_eloop():
except Exception: # pylint: disable=broad-except
pass
- six.print_('Starting event monitor,{} use ctrl-c to stop...'.format(device_filter_txt if device_filter_txt else ''))
+ six.print_('Starting {}event monitor,{} use ctrl-c to stop...'.format('PnP ' if pnp_context else '',
+ device_filter_txt if device_filter_txt else ''))
future.add_done_callback(lambda future: stop_and_suppress_eloop())
result = loop.run_until_complete(future)
except KeyboardInterrupt:
@@ -66,8 +71,9 @@ def stop_and_suppress_eloop():
raise RuntimeError(error)
-async def initiate_event_monitor(target, consumer_group, enqueued_time, device_id=None, properties=None,
- timeout=0, output=None, content_type=None, devices=None):
+async def initiate_event_monitor(target, consumer_group, enqueued_time, device_id=None,
+ properties=None, timeout=0, output=None, content_type=None,
+ devices=None, interface_id=None, pnp_context=None):
def _get_conn_props():
properties = {}
properties["product"] = "az.cli.iot.extension"
@@ -112,20 +118,22 @@ def _get_conn_props():
timeout=timeout,
output=output,
content_type=content_type,
- devices=devices))
+ devices=devices,
+ interface_id=interface_id,
+ pnp_context=pnp_context))
await asyncio.gather(*coroutines, return_exceptions=True)
-# pylint: disable=too-many-statements
+# pylint: disable=too-many-statements, too-many-branches
async def monitor_events(endpoint, connection, path, auth, partition, consumer_group, enqueuedtimeutc,
- properties, device_id=None, timeout=0, output=None, content_type=None, devices=None):
+ properties, device_id=None, timeout=0, output=None, content_type=None, devices=None,
+ interface_id=None, pnp_context=None):
source = uamqp.address.Source('amqps://{}/{}/ConsumerGroups/{}/Partitions/{}'.format(endpoint, path,
consumer_group, partition))
source.set_filter(
bytes('amqp.annotation.x-opt-enqueuedtimeutc > ' + str(enqueuedtimeutc), 'utf8'))
def _output_msg_kpi(msg):
- # TODO: Determine if amqp filters can support boolean operators for multiple conditions
origin = str(msg.annotations.get(b'iothub-connection-device-id'), 'utf8')
if device_id and device_id != origin:
if '*' in device_id or '?' in device_id:
@@ -137,6 +145,15 @@ def _output_msg_kpi(msg):
if devices and origin not in devices:
return
+ if pnp_context:
+ msg_interface_id = str(msg.annotations.get(b'iothub-interface-id'), 'utf8')
+ if not msg_interface_id:
+ return
+
+ if interface_id:
+ if msg_interface_id != interface_id:
+ return
+
event_source = {'event': {}}
event_source['event']['origin'] = origin
@@ -162,6 +179,23 @@ def _output_msg_kpi(msg):
event_source['event']['payload'] = payload
+ if pnp_context:
+ event_source['event']['interface'] = msg_interface_id
+
+ msg_schema = str(msg.application_properties.get(b'iothub-message-schema'), 'utf8')
+ interface_context = pnp_context['interface'].get(msg_interface_id)
+ if interface_context:
+ msg_schema_context = interface_context.get(msg_schema)
+ if msg_schema_context:
+ msg_context_display = msg_schema_context.get('display')
+ msg_context_unit = msg_schema_context.get('unit')
+
+ if msg_context_display:
+ event_source['event']['payload'] = {}
+ event_source['event']['payload'][msg_context_display] = payload
+ if msg_context_unit:
+ event_source['event']['payload']['unit'] = msg_context_unit
+
if 'anno' in properties or 'all' in properties:
event_source['event']['annotations'] = unicode_binary_map(msg.annotations)
if 'sys' in properties or 'all' in properties:
diff --git a/azext_iot/operations/generic.py b/azext_iot/operations/generic.py
index 87ccd22c8..da171236e 100644
--- a/azext_iot/operations/generic.py
+++ b/azext_iot/operations/generic.py
@@ -10,7 +10,7 @@
def _execute_query(query, query_method, top=None):
payload = []
- headers = {}
+ headers = {'Cache-Control': 'no-cache, must-revalidate'}
if top:
headers['x-ms-max-item-count'] = str(top)
diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py
index 31e47fb09..f9fe8c84b 100644
--- a/azext_iot/operations/hub.py
+++ b/azext_iot/operations/hub.py
@@ -11,7 +11,6 @@
from knack.log import get_logger
from knack.util import CLIError
from azure.cli.core.util import read_file_content
-from azext_iot.common.utility import calculate_millisec_since_unix_epoch_utc
from azext_iot._constants import (EXTENSION_ROOT,
BASE_API_VERSION,
DEVICE_DEVICESCOPE_PREFIX,
@@ -24,6 +23,7 @@
MetricType)
from azext_iot.common._azure import get_iot_hub_connection_string
from azext_iot.common.utility import (shell_safe_json_parse,
+ calculate_millisec_since_unix_epoch_utc,
validate_key_value_pairs, url_encode_dict,
evaluate_literal, unpack_msrest_error)
from azext_iot._factory import _bind_sdk
@@ -1288,6 +1288,38 @@ def iot_device_upload_file(cmd, device_id, file_path, content_type, hub_name=Non
def iot_hub_monitor_events(cmd, hub_name=None, device_id=None, consumer_group='$Default', timeout=300,
enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False,
login=None, content_type=None, device_query=None):
+ _iot_hub_monitor_events(cmd, interface=None, pnp_context=None, hub_name=hub_name, device_id=device_id,
+ consumer_group=consumer_group, timeout=timeout, enqueued_time=enqueued_time,
+ resource_group_name=resource_group_name, yes=yes, properties=properties,
+ repair=repair, login=login, content_type=content_type, device_query=device_query)
+
+
+def iot_hub_monitor_feedback(cmd, hub_name=None, device_id=None, yes=False,
+ wait_on_id=None, repair=False, resource_group_name=None, login=None):
+ from azext_iot.common.deps import ensure_uamqp
+ from azext_iot.common.utility import validate_min_python_version
+
+ validate_min_python_version(3, 4)
+
+ config = cmd.cli_ctx.config
+ ensure_uamqp(config, yes, repair)
+
+ target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
+
+ return _iot_hub_monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id)
+
+
+def iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None):
+ device_twin = _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name)
+ return _customize_device_tracing_output(device_twin['deviceId'], device_twin['properties']['desired'],
+ device_twin['properties']['reported'])
+
+
+# pylint: disable=too-many-locals
+def _iot_hub_monitor_events(cmd, interface=None, pnp_context=None,
+ hub_name=None, device_id=None, consumer_group='$Default', timeout=300,
+ enqueued_time=None, resource_group_name=None, yes=False, properties=None, repair=False,
+ login=None, content_type=None, device_query=None):
import importlib
from azext_iot.common.deps import ensure_uamqp
from azext_iot.common.utility import validate_min_python_version
@@ -1329,35 +1361,9 @@ def iot_hub_monitor_events(cmd, hub_name=None, device_id=None, consumer_group='$
device_id=device_id,
output=output,
content_type=content_type,
- devices=device_ids)
-
-
-def iot_hub_monitor_feedback(cmd, hub_name=None, device_id=None, yes=False,
- wait_on_id=None, repair=False, resource_group_name=None, login=None):
- from azext_iot.common.deps import ensure_uamqp
- from azext_iot.common.utility import validate_min_python_version
-
- validate_min_python_version(3, 4)
-
- config = cmd.cli_ctx.config
- ensure_uamqp(config, yes, repair)
-
- target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name, login=login)
-
- return _iot_hub_monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id)
-
-
-def _iot_hub_monitor_feedback(target, device_id, wait_on_id):
- import importlib
-
- events3 = importlib.import_module('azext_iot.operations.events3._events')
- events3.monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600)
-
-
-def iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None):
- device_twin = _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name)
- return _customize_device_tracing_output(device_twin['deviceId'], device_twin['properties']['desired'],
- device_twin['properties']['reported'])
+ devices=device_ids,
+ interface_id=interface,
+ pnp_context=pnp_context)
def iot_hub_distributed_tracing_update(cmd, hub_name, device_id, sampling_mode, sampling_rate,
@@ -1374,6 +1380,13 @@ def iot_hub_distributed_tracing_update(cmd, hub_name, device_id, sampling_mode,
result.properties.reported)
+def _iot_hub_monitor_feedback(target, device_id, wait_on_id):
+ import importlib
+
+ events3 = importlib.import_module('azext_iot.operations.events3._events')
+ events3.monitor_feedback(target=target, device_id=device_id, wait_on_id=wait_on_id, token_duration=3600)
+
+
def _iot_hub_distributed_tracing_show(cmd, hub_name, device_id, resource_group_name=None):
target = get_iot_hub_connection_string(cmd, hub_name, resource_group_name)
device_twin = iot_device_twin_show(cmd, device_id, hub_name, resource_group_name)
diff --git a/azext_iot/operations/pnp.py b/azext_iot/operations/pnp.py
new file mode 100644
index 000000000..e882d2cb7
--- /dev/null
+++ b/azext_iot/operations/pnp.py
@@ -0,0 +1,240 @@
+# coding=utf-8
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Unpublished works.
+# --------------------------------------------------------------------------------------------
+
+import json
+from os.path import exists
+from knack.log import get_logger
+from knack.util import CLIError
+from azext_iot.common.shared import SdkType, PnPModelType
+from azext_iot.common._azure import get_iot_pnp_connection_string
+from azext_iot.pnp_sdk.models import SearchOptions
+from azext_iot._factory import _bind_sdk
+from azext_iot._constants import PNP_API_VERSION, PNP_ENDPOINT
+from azext_iot.common.utility import (unpack_pnp_http_error,
+ get_sas_token,
+ shell_safe_json_parse)
+from azure.cli.core.util import read_file_content
+
+logger = get_logger(__name__)
+
+
+def iot_pnp_interface_publish(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ model_list = _iot_pnp_model_list(cmd, repo_endpoint, repo_id, interface, PnPModelType.interface,
+ -1, login=login)
+ if model_list and model_list[0].urn_id == interface:
+ etag = model_list[0].etag
+ else:
+ raise CLIError('No PnP Model definition found for @id "{}"'.format(interface))
+
+ target_interface = _iot_pnp_model_show(cmd, repo_endpoint, repo_id,
+ interface, False, PnPModelType.interface, login=login)
+
+ return _iot_pnp_model_publish(cmd, repo_endpoint, repo_id, interface, target_interface,
+ etag, login=login)
+
+
+def iot_pnp_interface_create(cmd, interface_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, interface_definition,
+ PnPModelType.interface, False, login=login)
+
+
+def iot_pnp_interface_update(cmd, interface_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, interface_definition,
+ PnPModelType.interface, True, login=login)
+
+
+def iot_pnp_interface_show(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ return _iot_pnp_model_show(cmd, repo_endpoint, repo_id,
+ interface, False, PnPModelType.interface, login=login)
+
+
+def iot_pnp_interface_list(cmd, repo_endpoint=PNP_ENDPOINT, repo_id=None, search_string=None,
+ top=1000, login=None):
+ return _iot_pnp_model_list(cmd, repo_endpoint, repo_id,
+ search_string, PnPModelType.interface,
+ top, login=login)
+
+
+def iot_pnp_interface_delete(cmd, interface, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ return _iot_pnp_model_delete(cmd, repo_endpoint, repo_id, interface, login)
+
+
+def iot_pnp_model_publish(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ model_list = _iot_pnp_model_list(cmd, repo_endpoint, repo_id, model, PnPModelType.capabilityModel,
+ -1, login=login)
+ if model_list and model_list[0].urn_id == model:
+ etag = model_list[0].etag
+ else:
+ raise CLIError('No PnP Model definition found for @id "{}"'.format(model))
+
+ target_model = _iot_pnp_model_show(cmd, repo_endpoint, repo_id,
+ model, False, PnPModelType.capabilityModel, login=login)
+ return _iot_pnp_model_publish(cmd, repo_endpoint, repo_id, model, target_model,
+ etag, login=login)
+
+
+def iot_pnp_model_create(cmd, model_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, model_definition,
+ PnPModelType.capabilityModel, False, login=login)
+
+
+def iot_pnp_model_update(cmd, model_definition, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ return _iot_pnp_model_create_or_update(cmd, repo_endpoint, repo_id, model_definition,
+ PnPModelType.capabilityModel, True, login=login)
+
+
+def iot_pnp_model_show(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, expand=False, login=None):
+ return _iot_pnp_model_show(cmd, repo_endpoint, repo_id,
+ model, expand, PnPModelType.capabilityModel, login=login)
+
+
+def iot_pnp_model_list(cmd, repo_endpoint=PNP_ENDPOINT, repo_id=None, search_string=None,
+ top=1000, login=None):
+ return _iot_pnp_model_list(cmd, repo_endpoint, repo_id,
+ search_string, PnPModelType.capabilityModel,
+ top, login=login)
+
+
+def iot_pnp_model_delete(cmd, model, repo_endpoint=PNP_ENDPOINT, repo_id=None, login=None):
+ _validate_repository(repo_id, login)
+ return _iot_pnp_model_delete(cmd, repo_endpoint, repo_id, model, login)
+
+
+def _iot_pnp_model_publish(cmd, endpoint, repository, model_id, model_def, etag, login):
+
+ target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login)
+ pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk)
+
+ contents = json.loads(json.dumps(model_def, separators=(',', ':'), indent=2))
+ try:
+ headers = get_sas_token(target)
+ return pnp_sdk.create_or_update_model(model_id,
+ api_version=PNP_API_VERSION,
+ content=contents,
+ if_match=etag,
+ custom_headers=headers)
+ except errors.HttpOperationError as e:
+ raise CLIError(unpack_pnp_http_error(e))
+
+
+def _iot_pnp_model_create_or_update(cmd, endpoint, repository, model_def, pnpModelType, is_update, login):
+
+ target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login)
+ pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk)
+ etag = None
+ model_def = _validate_model_definition(model_def)
+ model_id = model_def.get('@id')
+ if not model_id:
+ raise CLIError('PnP Model definition requires @id! Please include @id and try again.')
+
+ if is_update:
+ model_list = _iot_pnp_model_list(cmd, endpoint, repository, model_id, pnpModelType,
+ -1, login=login)
+ if model_list and model_list[0].urn_id == model_id:
+ etag = model_list[0].etag
+ else:
+ raise CLIError('No PnP Model definition found for @id "{}"'.format(model_id))
+
+ contents = json.loads(json.dumps(model_def, separators=(',', ':'), indent=2))
+ try:
+ headers = get_sas_token(target)
+ return pnp_sdk.create_or_update_model(model_id,
+ api_version=PNP_API_VERSION,
+ content=contents,
+ repository_id=target.get('repository_id', None),
+ if_match=etag,
+ custom_headers=headers)
+ except errors.HttpOperationError as e:
+ raise CLIError(unpack_pnp_http_error(e))
+
+
+def _iot_pnp_model_show(cmd, endpoint, repository, model_id, expand, pnpModelType, login):
+ target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login)
+ pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk)
+ try:
+ headers = get_sas_token(target)
+ result = pnp_sdk.get_model(model_id, api_version=PNP_API_VERSION,
+ repository_id=target.get('repository_id', None),
+ custom_headers=headers,
+ expand=expand)
+
+ if not result or result["@type"].lower() != pnpModelType.value.lower():
+ raise CLIError('PnP Model definition for "{}", not found.'.format(model_id))
+
+ return result
+ except errors.HttpOperationError as e:
+ raise CLIError(unpack_pnp_http_error(e))
+
+
+def _iot_pnp_model_list(cmd, endpoint, repository, search_string,
+ pnpModelType, top, login):
+ target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login)
+
+ pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk)
+ try:
+ headers = get_sas_token(target)
+ search_options = SearchOptions(search_keyword=search_string,
+ model_filter_type=pnpModelType.value)
+ if top > 0:
+ search_options.page_size = top
+
+ result = pnp_sdk.search(search_options, api_version=PNP_API_VERSION,
+ repository_id=target.get('repository_id', None),
+ custom_headers=headers)
+ return result.results
+ except errors.HttpOperationError as e:
+ raise CLIError(unpack_pnp_http_error(e))
+
+
+def _iot_pnp_model_delete(cmd, endpoint, repository, model_id, login):
+ target = get_iot_pnp_connection_string(cmd, endpoint, repository, login=login)
+
+ pnp_sdk, errors = _bind_sdk(target, SdkType.pnp_sdk)
+ try:
+ headers = get_sas_token(target)
+ return pnp_sdk.delete_model(model_id,
+ repository_id=target.get('repository_id', None),
+ api_version=PNP_API_VERSION,
+ custom_headers=headers)
+ except errors.HttpOperationError as e:
+ raise CLIError(unpack_pnp_http_error(e))
+
+
+def _looks_like_file(element):
+ element = element.lower()
+ if element.endswith(('.txt', '.json', '.md', '.rst', '.doc', '.docx')):
+ return True
+ return False
+
+
+def _validate_model_definition(model_def):
+ if exists(model_def):
+ model_def = str(read_file_content(model_def))
+ else:
+ logger.info('Definition not from file path or incorrect path given.')
+
+ try:
+ return shell_safe_json_parse(model_def)
+ except ValueError as e:
+ logger.debug('Received definition: %s', model_def)
+ if _looks_like_file(model_def):
+ raise CLIError('The definition content looks like its from a file. Please ensure the path is correct.')
+ raise CLIError('Malformed capability model definition. '
+ 'Use --debug to see what was received. Error details: {}'.format(e))
+
+
+def _validate_repository(repo_id, login):
+ if not login and not repo_id:
+ raise CLIError('Please provide the model repository\'s repositoryId (via the \'--repo-id\' or \'-r\' parameter)'
+ ' and endpoint (via the \'--endpoint\' or \'-e\' parameter) or model repository\'s connection'
+ ' string via --login...')
diff --git a/azext_iot/pnp_sdk/__init__.py b/azext_iot/pnp_sdk/__init__.py
new file mode 100644
index 000000000..b77741974
--- /dev/null
+++ b/azext_iot/pnp_sdk/__init__.py
@@ -0,0 +1,18 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from .digital_twin_repository_service import DigitalTwinRepositoryService
+from .version import VERSION
+
+__all__ = ['DigitalTwinRepositoryService']
+
+__version__ = VERSION
+
diff --git a/azext_iot/pnp_sdk/digital_twin_repository_service.py b/azext_iot/pnp_sdk/digital_twin_repository_service.py
new file mode 100644
index 000000000..b1250f91a
--- /dev/null
+++ b/azext_iot/pnp_sdk/digital_twin_repository_service.py
@@ -0,0 +1,360 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.service_client import SDKClient
+from msrest import Configuration, Serializer, Deserializer
+from .version import VERSION
+from msrest.pipeline import ClientRawResponse
+from msrest.exceptions import HttpOperationError
+from . import models
+
+
+class DigitalTwinRepositoryServiceConfiguration(Configuration):
+ """Configuration for DigitalTwinRepositoryService
+ Note that all parameters used to create this instance are saved as instance
+ attributes.
+
+ :param str base_url: Service URL
+ """
+
+ def __init__(
+ self, base_url=None):
+
+ if not base_url:
+ base_url = 'http://localhost'
+
+ super(DigitalTwinRepositoryServiceConfiguration, self).__init__(base_url)
+
+ self.add_user_agent('digitaltwinrepositoryservice/{}'.format(VERSION))
+
+
+class DigitalTwinRepositoryService(SDKClient):
+ """DigitalTwin Model Repository Service.
+
+ :ivar config: Configuration for client.
+ :vartype config: DigitalTwinRepositoryServiceConfiguration
+
+ :param str base_url: Service URL
+ """
+
+ def __init__(
+ self, base_url=None):
+
+ self.config = DigitalTwinRepositoryServiceConfiguration(base_url)
+ super(DigitalTwinRepositoryService, self).__init__(None, self.config)
+
+ client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)}
+ self.api_version = 'v1'
+ self._serialize = Serializer(client_models)
+ self._deserialize = Deserializer(client_models)
+
+
+ def get_model(
+ self, model_id, api_version, repository_id=None, x_ms_client_request_id=None, expand=False, custom_headers=None, raw=False, **operation_config):
+ """Returns a DigitalTwin model object for the given \"id\".\r\nIf
+ \"expand\" is present in the query parameters and \"id\" is for a
+ capability model then it returns\r\nthe capability model with expanded
+ interface definitions.
+
+ :param model_id: Model id Ex:
+ urn:contoso:com:temparaturesensor:1
+ :type model_id: str
+ :param api_version: Version of the Api. Must be 2019-07-01-preview
+ :type api_version: str
+ :param repository_id: To access private repo, repositoryId is the repo
+ id. To access global repo, caller should not specify this value.
+ :type repository_id: str
+ :param x_ms_client_request_id: Optional. Provides a client-generated
+ opaque value that is recorded in the logs. Using this header is highly
+ recommended for correlating client-side activities with requests
+ received by the server.
+ :type x_ms_client_request_id: str
+ :param expand: Indicates whether to expand the capability model's
+ interface definitions inline or not. This query parameter ONLY applies
+ to Capability model.
+ :type expand: bool
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: object or ClientRawResponse if raw=true
+ :rtype: object or ~msrest.pipeline.ClientRawResponse
+ :raises:
+ :class:`HttpOperationError`
+ """
+ # Construct URL
+ url = self.get_model.metadata['url']
+ path_format_arguments = {
+ 'modelId': self._serialize.url("model_id", model_id, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ if repository_id is not None:
+ query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str')
+ query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str')
+ if expand is not None:
+ query_parameters['expand'] = self._serialize.query("expand", expand, 'bool')
+
+ # Construct headers
+ header_parameters = {}
+ header_parameters['Accept'] = 'application/ld+json'
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if x_ms_client_request_id is not None:
+ header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str')
+
+ # Construct and send request
+ request = self._client.get(url, query_parameters, header_parameters)
+ response = self._client.send(request, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ raise HttpOperationError(self._deserialize, response)
+
+ deserialized = None
+ header_dict = {}
+
+ if response.status_code == 200:
+ deserialized = self._deserialize('{object}', response)
+ header_dict = {
+ 'x-ms-request-id': 'str',
+ 'ETag': 'str',
+ 'x-ms-model-id': 'str',
+ 'x-ms-model-publisher-id': 'str',
+ 'x-ms-model-publisher-name': 'str',
+ 'x-ms-model-createdon': 'iso-8601',
+ 'x-ms-model-lastupdated': 'iso-8601',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ get_model.metadata = {'url': '/models/{modelId}'}
+
+ def create_or_update_model(
+ self, model_id, api_version, content, repository_id=None, x_ms_client_request_id=None, if_match=None, custom_headers=None, raw=False, **operation_config):
+ """Creates or updates the DigitalTwin Model in the repository.
+
+ :param model_id: Model id Ex:
+ urn:contoso:TemparatureSensor:1
+ :type model_id: str
+ :param api_version: Version of the Api. Must be 2019-07-01-preview
+ :type api_version: str
+ :param content: Model definition in Digital Twin Definition Language
+ format.
+ :type content: object
+ :param repository_id: To access private repo, repositoryId is the repo
+ id\\r\\nTo access global repo, caller should not specify this value.
+ :type repository_id: str
+ :param x_ms_client_request_id: Optional. Provides a client-generated
+ opaque value that is recorded in the logs. Using this header is highly
+ recommended for correlating client-side activities with requests
+ received by the server.
+ :type x_ms_client_request_id: str
+ :param if_match: Used to make operation conditional for optimistic
+ concurrency. That is, the document is updated only if the specified
+ etag matches the current version in the database. The value should be
+ set to the etag value of the resource.
+ :type if_match: str
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: None or ClientRawResponse if raw=true
+ :rtype: None or ~msrest.pipeline.ClientRawResponse
+ :raises:
+ :class:`HttpOperationError`
+ """
+ # Construct URL
+ url = self.create_or_update_model.metadata['url']
+ path_format_arguments = {
+ 'modelId': self._serialize.url("model_id", model_id, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ if repository_id is not None:
+ query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str')
+ query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ header_parameters['Content-Type'] = 'application/json; charset=utf-8'
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if x_ms_client_request_id is not None:
+ header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str')
+ if if_match is not None:
+ header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str')
+
+ # Construct body
+ body_content = self._serialize.body(content, 'object')
+ # Construct and send request
+ request = self._client.put(url, query_parameters, header_parameters, body_content)
+ response = self._client.send(request, stream=False, **operation_config)
+
+ if response.status_code not in [201, 204, 412]:
+ raise HttpOperationError(self._deserialize, response)
+
+ if raw:
+ client_raw_response = ClientRawResponse(None, response)
+ client_raw_response.add_headers({
+ 'x-ms-request-id': 'str',
+ 'ETag': 'str',
+ })
+ return client_raw_response
+ create_or_update_model.metadata = {'url': '/models/{modelId}'}
+
+ def delete_model(
+ self, model_id, repository_id, api_version, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config):
+ """Deletes a digital twin model from the repository.
+
+ :param model_id: Model id Ex:
+ urn:contoso:com:temparaturesensor:1
+ :type model_id: str
+ :param repository_id: To access private repo, repositoryId is the repo
+ id. Delete is not allowed for public repository.
+ :type repository_id: str
+ :param api_version: Version of the Api. Must be 2019-07-01-preview
+ :type api_version: str
+ :param x_ms_client_request_id: Optional. Provides a client-generated
+ opaque value that is recorded in the logs. Using this header is highly
+ recommended for correlating client-side activities with requests
+ received by the server.
+ :type x_ms_client_request_id: str
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: None or ClientRawResponse if raw=true
+ :rtype: None or ~msrest.pipeline.ClientRawResponse
+ :raises:
+ :class:`HttpOperationError`
+ """
+ # Construct URL
+ url = self.delete_model.metadata['url']
+ path_format_arguments = {
+ 'modelId': self._serialize.url("model_id", model_id, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str')
+ query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if x_ms_client_request_id is not None:
+ header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str')
+
+ # Construct and send request
+ request = self._client.delete(url, query_parameters, header_parameters)
+ response = self._client.send(request, stream=False, **operation_config)
+
+ if response.status_code not in [204]:
+ raise HttpOperationError(self._deserialize, response)
+
+ if raw:
+ client_raw_response = ClientRawResponse(None, response)
+ client_raw_response.add_headers({
+ 'x-ms-request-id': 'str',
+ })
+ return client_raw_response
+ delete_model.metadata = {'url': '/models/{modelId}'}
+
+ def search(
+ self, search_options, api_version, repository_id=None, x_ms_client_request_id=None, custom_headers=None, raw=False, **operation_config):
+ """Searches pnp models for given search options.
+ It searches in the "Description, DisplayName, Comment and Id" metadata.
+
+ :param search_options: Set SearchOption.searchKeyword to search models
+ with the keyword.
+ Set the "SearchOptions.modelFilterType" to restrict to a type of
+ DigitalTwin model (Ex: Interface or CapabilityModel).
+ Default it returns all the models.
+ :type search_options:
+ ~digitaltwinmodelrepositoryservice.models.SearchOptions
+ :param api_version: Version of the Api. Must be 2019-07-01-preview
+ :type api_version: str
+ :param repository_id: To access private repo, repositoryId is the repo
+ id.\\r\\nDelete is not allowed for public repository.
+ :type repository_id: str
+ :param x_ms_client_request_id: Optional. Provides a client-generated
+ opaque value that is recorded in the logs. Using this header is highly
+ recommended for correlating client-side activities with requests
+ received by the server..
+ :type x_ms_client_request_id: str
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: SearchResponse or ClientRawResponse if raw=true
+ :rtype: ~digitaltwinmodelrepositoryservice.models.SearchResponse or
+ ~msrest.pipeline.ClientRawResponse
+ :raises:
+ :class:`HttpOperationError`
+ """
+ # Construct URL
+ url = self.search.metadata['url']
+
+ # Construct parameters
+ query_parameters = {}
+ if repository_id is not None:
+ query_parameters['repositoryId'] = self._serialize.query("repository_id", repository_id, 'str')
+ query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ header_parameters['Accept'] = 'application/json'
+ header_parameters['Content-Type'] = 'application/json; charset=utf-8'
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if x_ms_client_request_id is not None:
+ header_parameters['x-ms-client-request-id'] = self._serialize.header("x_ms_client_request_id", x_ms_client_request_id, 'str')
+
+ # Construct body
+ body_content = self._serialize.body(search_options, 'SearchOptions')
+
+ # Construct and send request
+ request = self._client.post(url, query_parameters, header_parameters, body_content)
+ response = self._client.send(request, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ raise HttpOperationError(self._deserialize, response)
+
+ deserialized = None
+ header_dict = {}
+
+ if response.status_code == 200:
+ deserialized = self._deserialize('SearchResponse', response)
+ header_dict = {
+ 'x-ms-request-id': 'str',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ search.metadata = {'url': '/models/search'}
diff --git a/azext_iot/pnp_sdk/models/__init__.py b/azext_iot/pnp_sdk/models/__init__.py
new file mode 100644
index 000000000..21a611417
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/__init__.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+try:
+ from .search_options_py3 import SearchOptions
+ from .model_information_py3 import ModelInformation
+ from .search_response_py3 import SearchResponse
+except (SyntaxError, ImportError):
+ from .search_options import SearchOptions
+ from .model_information import ModelInformation
+ from .search_response import SearchResponse
+
+__all__ = [
+ 'SearchOptions',
+ 'ModelInformation',
+ 'SearchResponse',
+]
diff --git a/azext_iot/pnp_sdk/models/model_information.py b/azext_iot/pnp_sdk/models/model_information.py
new file mode 100644
index 000000000..c3f804945
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/model_information.py
@@ -0,0 +1,72 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class ModelInformation(Model):
+ """ModelInformation.
+
+ :param comment:
+ :type comment: str
+ :param description:
+ :type description: str
+ :param display_name:
+ :type display_name: str
+ :param urn_id:
+ :type urn_id: str
+ :param model_name:
+ :type model_name: str
+ :param version:
+ :type version: int
+ :param type: Possible values include: 'interface', 'capabilityModel'
+ :type type: str or ~digitaltwinmodelrepositoryservice.models.enum
+ :param etag:
+ :type etag: str
+ :param publisher_id:
+ :type publisher_id: str
+ :param publisher_name:
+ :type publisher_name: str
+ :param created_on:
+ :type created_on: datetime
+ :param updated_on:
+ :type updated_on: datetime
+ """
+
+ _attribute_map = {
+ 'comment': {'key': 'comment', 'type': 'str'},
+ 'description': {'key': 'description', 'type': 'str'},
+ 'display_name': {'key': 'displayName', 'type': 'str'},
+ 'urn_id': {'key': 'urnId', 'type': 'str'},
+ 'model_name': {'key': 'modelName', 'type': 'str'},
+ 'version': {'key': 'version', 'type': 'int'},
+ 'type': {'key': 'type', 'type': 'str'},
+ 'etag': {'key': 'etag', 'type': 'str'},
+ 'publisher_id': {'key': 'publisherId', 'type': 'str'},
+ 'publisher_name': {'key': 'publisherName', 'type': 'str'},
+ 'created_on': {'key': 'createdOn', 'type': 'iso-8601'},
+ 'updated_on': {'key': 'updatedOn', 'type': 'iso-8601'},
+ }
+
+ def __init__(self, **kwargs):
+ super(ModelInformation, self).__init__(**kwargs)
+ self.comment = kwargs.get('comment', None)
+ self.description = kwargs.get('description', None)
+ self.display_name = kwargs.get('display_name', None)
+ self.urn_id = kwargs.get('urn_id', None)
+ self.model_name = kwargs.get('model_name', None)
+ self.version = kwargs.get('version', None)
+ self.type = kwargs.get('type', None)
+ self.etag = kwargs.get('etag', None)
+ self.publisher_id = kwargs.get('publisher_id', None)
+ self.publisher_name = kwargs.get('publisher_name', None)
+ self.created_on = kwargs.get('created_on', None)
+ self.updated_on = kwargs.get('updated_on', None)
diff --git a/azext_iot/pnp_sdk/models/model_information_py3.py b/azext_iot/pnp_sdk/models/model_information_py3.py
new file mode 100644
index 000000000..4ec8acd27
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/model_information_py3.py
@@ -0,0 +1,72 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class ModelInformation(Model):
+ """ModelInformation.
+
+ :param comment:
+ :type comment: str
+ :param description:
+ :type description: str
+ :param display_name:
+ :type display_name: str
+ :param urn_id:
+ :type urn_id: str
+ :param model_name:
+ :type model_name: str
+ :param version:
+ :type version: int
+ :param type: Possible values include: 'interface', 'capabilityModel'
+ :type type: str or ~digitaltwinmodelrepositoryservice.models.enum
+ :param etag:
+ :type etag: str
+ :param publisher_id:
+ :type publisher_id: str
+ :param publisher_name:
+ :type publisher_name: str
+ :param created_on:
+ :type created_on: datetime
+ :param updated_on:
+ :type updated_on: datetime
+ """
+
+ _attribute_map = {
+ 'comment': {'key': 'comment', 'type': 'str'},
+ 'description': {'key': 'description', 'type': 'str'},
+ 'display_name': {'key': 'displayName', 'type': 'str'},
+ 'urn_id': {'key': 'urnId', 'type': 'str'},
+ 'model_name': {'key': 'modelName', 'type': 'str'},
+ 'version': {'key': 'version', 'type': 'int'},
+ 'type': {'key': 'type', 'type': 'str'},
+ 'etag': {'key': 'etag', 'type': 'str'},
+ 'publisher_id': {'key': 'publisherId', 'type': 'str'},
+ 'publisher_name': {'key': 'publisherName', 'type': 'str'},
+ 'created_on': {'key': 'createdOn', 'type': 'iso-8601'},
+ 'updated_on': {'key': 'updatedOn', 'type': 'iso-8601'},
+ }
+
+ def __init__(self, *, comment: str=None, description: str=None, display_name: str=None, urn_id: str=None, model_name: str=None, version: int=None, type=None, etag: str=None, publisher_id: str=None, publisher_name: str=None, created_on=None, updated_on=None, **kwargs) -> None:
+ super(ModelInformation, self).__init__(**kwargs)
+ self.comment = comment
+ self.description = description
+ self.display_name = display_name
+ self.urn_id = urn_id
+ self.model_name = model_name
+ self.version = version
+ self.type = type
+ self.etag = etag
+ self.publisher_id = publisher_id
+ self.publisher_name = publisher_name
+ self.created_on = created_on
+ self.updated_on = updated_on
diff --git a/azext_iot/pnp_sdk/models/search_options.py b/azext_iot/pnp_sdk/models/search_options.py
new file mode 100644
index 000000000..497740e04
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/search_options.py
@@ -0,0 +1,42 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class SearchOptions(Model):
+ """SearchOptions.
+
+ :param search_keyword:
+ :type search_keyword: str
+ :param model_filter_type: Possible values include: 'interface',
+ 'capabilityModel'
+ :type model_filter_type: str or
+ ~digitaltwinmodelrepositoryservice.models.enum
+ :param continuation_token:
+ :type continuation_token: str
+ :param page_size:
+ :type page_size: int
+ """
+
+ _attribute_map = {
+ 'search_keyword': {'key': 'searchKeyword', 'type': 'str'},
+ 'model_filter_type': {'key': 'modelFilterType', 'type': 'str'},
+ 'continuation_token': {'key': 'continuationToken', 'type': 'str'},
+ 'page_size': {'key': 'pageSize', 'type': 'int'},
+ }
+
+ def __init__(self, **kwargs):
+ super(SearchOptions, self).__init__(**kwargs)
+ self.search_keyword = kwargs.get('search_keyword', None)
+ self.model_filter_type = kwargs.get('model_filter_type', None)
+ self.continuation_token = kwargs.get('continuation_token', None)
+ self.page_size = kwargs.get('page_size', None)
diff --git a/azext_iot/pnp_sdk/models/search_options_py3.py b/azext_iot/pnp_sdk/models/search_options_py3.py
new file mode 100644
index 000000000..f6da4fd89
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/search_options_py3.py
@@ -0,0 +1,42 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class SearchOptions(Model):
+ """SearchOptions.
+
+ :param search_keyword:
+ :type search_keyword: str
+ :param model_filter_type: Possible values include: 'interface',
+ 'capabilityModel'
+ :type model_filter_type: str or
+ ~digitaltwinmodelrepositoryservice.models.enum
+ :param continuation_token:
+ :type continuation_token: str
+ :param page_size:
+ :type page_size: int
+ """
+
+ _attribute_map = {
+ 'search_keyword': {'key': 'searchKeyword', 'type': 'str'},
+ 'model_filter_type': {'key': 'modelFilterType', 'type': 'str'},
+ 'continuation_token': {'key': 'continuationToken', 'type': 'str'},
+ 'page_size': {'key': 'pageSize', 'type': 'int'},
+ }
+
+ def __init__(self, *, search_keyword: str=None, model_filter_type=None, continuation_token: str=None, page_size: int=None, **kwargs) -> None:
+ super(SearchOptions, self).__init__(**kwargs)
+ self.search_keyword = search_keyword
+ self.model_filter_type = model_filter_type
+ self.continuation_token = continuation_token
+ self.page_size = page_size
diff --git a/azext_iot/pnp_sdk/models/search_response.py b/azext_iot/pnp_sdk/models/search_response.py
new file mode 100644
index 000000000..a6e36d521
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/search_response.py
@@ -0,0 +1,33 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class SearchResponse(Model):
+ """SearchResponse.
+
+ :param continuation_token:
+ :type continuation_token: str
+ :param results:
+ :type results:
+ list[~digitaltwinmodelrepositoryservice.models.ModelInformation]
+ """
+
+ _attribute_map = {
+ 'continuation_token': {'key': 'continuationToken', 'type': 'str'},
+ 'results': {'key': 'results', 'type': '[ModelInformation]'},
+ }
+
+ def __init__(self, **kwargs):
+ super(SearchResponse, self).__init__(**kwargs)
+ self.continuation_token = kwargs.get('continuation_token', None)
+ self.results = kwargs.get('results', None)
diff --git a/azext_iot/pnp_sdk/models/search_response_py3.py b/azext_iot/pnp_sdk/models/search_response_py3.py
new file mode 100644
index 000000000..94d05c147
--- /dev/null
+++ b/azext_iot/pnp_sdk/models/search_response_py3.py
@@ -0,0 +1,33 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class SearchResponse(Model):
+ """SearchResponse.
+
+ :param continuation_token:
+ :type continuation_token: str
+ :param results:
+ :type results:
+ list[~digitaltwinmodelrepositoryservice.models.ModelInformation]
+ """
+
+ _attribute_map = {
+ 'continuation_token': {'key': 'continuationToken', 'type': 'str'},
+ 'results': {'key': 'results', 'type': '[ModelInformation]'},
+ }
+
+ def __init__(self, *, continuation_token: str=None, results=None, **kwargs) -> None:
+ super(SearchResponse, self).__init__(**kwargs)
+ self.continuation_token = continuation_token
+ self.results = results
diff --git a/azext_iot/pnp_sdk/version.py b/azext_iot/pnp_sdk/version.py
new file mode 100644
index 000000000..8bf1b66a3
--- /dev/null
+++ b/azext_iot/pnp_sdk/version.py
@@ -0,0 +1,13 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+VERSION = "v1"
+
diff --git a/azext_iot/service_sdk/iot_hub_gateway_service_apis.py b/azext_iot/service_sdk/iot_hub_gateway_service_apis.py
index d7faaa391..1be0cdb83 100644
--- a/azext_iot/service_sdk/iot_hub_gateway_service_apis.py
+++ b/azext_iot/service_sdk/iot_hub_gateway_service_apis.py
@@ -18,7 +18,7 @@
import uuid
from . import models
from azext_iot._constants import VERSION as extver
-from azext_iot._constants import BASE_API_VERSION
+from azext_iot._constants import BASE_API_VERSION, PNP_API_VERSION
class IotHubGatewayServiceAPIsConfiguration(AzureConfiguration):
@@ -2357,3 +2357,376 @@ def invoke_device_method1(
# @digimaun - change device param from {id} to {deviceId}
invoke_device_method1.metadata = {'url': '/twins/{deviceId}/modules/{moduleId}/methods'}
+
+ def get_interfaces(
+ self, digital_twin_id, custom_headers=None, raw=False, **operation_config):
+ """Gets the list of interfaces.
+
+ :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is
+ DeviceId[~ModuleId]. ModuleId is optional.
+ :type digital_twin_id: str
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: DigitalTwinInterfaces or ClientRawResponse if raw=true
+ :rtype: ~service.models.DigitalTwinInterfaces or
+ ~msrest.pipeline.ClientRawResponse
+ :raises: class:`CloudError`
+ """
+ # Construct URL
+ url = self.get_interfaces.metadata['url']
+ path_format_arguments = {
+ 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released
+ query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ header_parameters['Content-Type'] = 'application/json; charset=utf-8'
+ if self.config.generate_client_request_id:
+ header_parameters['x-ms-client-request-id'] = str(uuid.uuid1())
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if self.config.accept_language is not None:
+ header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str')
+
+ # Construct and send request
+ request = self._client.get(url, query_parameters)
+ response = self._client.send(request, header_parameters, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ exp = CloudError(response)
+ exp.request_id = response.headers.get('x-ms-request-id')
+ raise exp
+
+ deserialized = None
+ header_dict = {}
+
+ # @anusapan - deserialize as {object} from DigitalTwinInterfaces
+ if response.status_code == 200:
+ deserialized = self._deserialize('{object}', response)
+ header_dict = {
+ 'ETag': 'str',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ get_interfaces.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'}
+
+ def update_interfaces(
+ self, digital_twin_id, if_match=None, interfaces=None, custom_headers=None, raw=False, **operation_config):
+ """Updates desired properties of multiple interfaces.
+ Example URI: "digitalTwins/{digitalTwinId}/interfaces".
+
+ :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is
+ DeviceId[~ModuleId]. ModuleId is optional.
+ :type digital_twin_id: str
+ :param if_match:
+ :type if_match: str
+ :param interfaces: Interface(s) data to patch in the digital twin.
+ :type interfaces: dict[str,
+ ~service.models.DigitalTwinInterfacesPatchInterfacesValue]
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: DigitalTwinInterfaces or ClientRawResponse if raw=true
+ :rtype: ~service.models.DigitalTwinInterfaces or
+ ~msrest.pipeline.ClientRawResponse
+ :raises: class:`CloudError`
+ """
+ interfaces_patch_info = models.DigitalTwinInterfacesPatch(interfaces=interfaces)
+
+ # Construct URL
+ url = self.update_interfaces.metadata['url']
+ path_format_arguments = {
+ 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released
+ query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ header_parameters['Content-Type'] = 'application/json; charset=utf-8'
+ if self.config.generate_client_request_id:
+ header_parameters['x-ms-client-request-id'] = str(uuid.uuid1())
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if self.config.accept_language is not None:
+ header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str')
+
+ if if_match is not None:
+ header_parameters['If-Match'] = self._serialize.header("if_match", if_match, 'str')
+
+ # Construct body
+ body_content = self._serialize.body(interfaces_patch_info, 'DigitalTwinInterfacesPatch')
+
+ # Construct and send request
+ request = self._client.patch(url, query_parameters)
+ response = self._client.send(request, header_parameters, body_content, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ exp = CloudError(response)
+ exp.request_id = response.headers.get('x-ms-request-id')
+ raise exp
+
+
+ deserialized = None
+ header_dict = {}
+
+ # @anusapan - deserialize as {object} from DigitalTwinInterfaces
+ if response.status_code == 200:
+ deserialized = self._deserialize('{object}', response)
+ header_dict = {
+ 'ETag': 'str',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ update_interfaces.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces'}
+
+ def get_interface(
+ self, digital_twin_id, interface_name, custom_headers=None, raw=False, **operation_config):
+ """Gets the interface of given interfaceId.
+ Example URI: "digitalTwins/{digitalTwinId}/interfaces/{interfaceName}".
+
+ :param digital_twin_id: Digital Twin ID. Format of digitalTwinId is
+ DeviceId[~ModuleId]. ModuleId is optional.
+ :type digital_twin_id: str
+ :param interface_name: The interface name.
+ :type interface_name: str
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: DigitalTwinInterfaces or ClientRawResponse if raw=true
+ :rtype: ~service.models.DigitalTwinInterfaces or
+ ~msrest.pipeline.ClientRawResponse
+ :raises: class:`CloudError`
+ """
+ # Construct URL
+ url = self.get_interface.metadata['url']
+ path_format_arguments = {
+ 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'),
+ 'interfaceName': self._serialize.url("interface_name", interface_name, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released
+ query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ if self.config.generate_client_request_id:
+ header_parameters['x-ms-client-request-id'] = str(uuid.uuid1())
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if self.config.accept_language is not None:
+ header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str')
+
+ # Construct and send request
+ request = self._client.get(url, query_parameters)
+ response = self._client.send(request, header_parameters, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ exp = CloudError(response)
+ exp.request_id = response.headers.get('x-ms-request-id')
+ raise exp
+
+ deserialized = None
+ header_dict = {}
+
+ # @anusapan - deserialize as {object} from DigitalTwinInterfaces
+ if response.status_code == 200:
+ deserialized = self._deserialize('{object}', response)
+ header_dict = {
+ 'ETag': 'str',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ get_interface.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}'}
+
+ def get_digital_twin_model(
+ self, model_id, expand=None, custom_headers=None, raw=False, **operation_config):
+ """Returns a DigitalTwin model definition for the given id.
+ If "expand" is present in the query parameters and id is for a device
+ capability model then it returns
+ the capability metamodel with expanded interface definitions.
+
+ :param model_id: Model id Ex:
+ urn:contoso:TemperatureSensor:1
+ :type model_id: str
+ :param expand: Indicates whether to expand the device capability
+ model's interface definitions inline or not.
+ This query parameter ONLY applies to Capability model.
+ :type expand: bool
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: object or ClientRawResponse if raw=true
+ :rtype: object or ~msrest.pipeline.ClientRawResponse
+ :raises: class:`CloudError`
+ """
+ # Construct URL
+ url = self.get_digital_twin_model.metadata['url']
+ path_format_arguments = {
+ 'modelId': self._serialize.url("model_id", model_id, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ if expand is not None:
+ query_parameters['expand'] = self._serialize.query("expand", expand, 'bool')
+ # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released
+ query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str')
+
+ # Construct headers
+ header_parameters = {}
+ if self.config.generate_client_request_id:
+ header_parameters['x-ms-client-request-id'] = str(uuid.uuid1())
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if self.config.accept_language is not None:
+ header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str')
+
+ # Construct and send request
+ request = self._client.get(url, query_parameters)
+ response = self._client.send(request, header_parameters, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ exp = CloudError(response)
+ exp.request_id = response.headers.get('x-ms-request-id')
+ raise exp
+
+ deserialized = None
+ header_dict = {}
+
+ if response.status_code == 200:
+ deserialized = self._deserialize('object', response)
+ header_dict = {
+ 'ETag': 'str',
+ 'x-ms-model-id': 'str',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ get_digital_twin_model.metadata = {'url': '/digitalTwins/models/{modelId}'}
+
+ def invoke_interface_command(
+ self, digital_twin_id, interface_name, command_name, payload, connect_timeout_in_seconds=None, response_timeout_in_seconds=None, custom_headers=None, raw=False, **operation_config):
+ """Invoke a digital twin interface command.
+
+ Invoke a digital twin interface command.
+
+ :param digital_twin_id:
+ :type digital_twin_id: str
+ :param interface_name:
+ :type interface_name: str
+ :param command_name:
+ :type command_name: str
+ :param payload:
+ :type payload: object
+ :param connect_timeout_in_seconds: Connect timeout in seconds.
+ :type connect_timeout_in_seconds: int
+ :param response_timeout_in_seconds: Response timeout in seconds.
+ :type response_timeout_in_seconds: int
+ :param dict custom_headers: headers that will be added to the request
+ :param bool raw: returns the direct response alongside the
+ deserialized response
+ :param operation_config: :ref:`Operation configuration
+ overrides`.
+ :return: object or ClientRawResponse if raw=true
+ :rtype: object or ~msrest.pipeline.ClientRawResponse
+ :raises: class:`CloudError`
+ """
+ # Construct URL
+ url = self.invoke_interface_command.metadata['url']
+ path_format_arguments = {
+ 'digitalTwinId': self._serialize.url("digital_twin_id", digital_twin_id, 'str'),
+ 'interfaceName': self._serialize.url("interface_name", interface_name, 'str'),
+ 'commandName': self._serialize.url("command_name", command_name, 'str')
+ }
+ url = self._client.format_url(url, **path_format_arguments)
+
+ # Construct parameters
+ query_parameters = {}
+ # @anusapan - Changed self.api_version to PNP_API_VERSION until new version get released
+ query_parameters['api-version'] = self._serialize.query("self.api_version", PNP_API_VERSION, 'str')
+ if connect_timeout_in_seconds is not None:
+ query_parameters['connectTimeoutInSeconds'] = self._serialize.query("connect_timeout_in_seconds", connect_timeout_in_seconds, 'int')
+ if response_timeout_in_seconds is not None:
+ query_parameters['responseTimeoutInSeconds'] = self._serialize.query("response_timeout_in_seconds", response_timeout_in_seconds, 'int')
+
+ # Construct headers
+ header_parameters = {}
+ if self.config.generate_client_request_id:
+ header_parameters['x-ms-client-request-id'] = str(uuid.uuid1())
+ if custom_headers:
+ header_parameters.update(custom_headers)
+ if self.config.accept_language is not None:
+ header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str')
+
+ # Construct body
+ body_content = self._serialize.body(payload, 'object')
+
+ # Construct and send request
+ request = self._client.post(url, query_parameters)
+ response = self._client.send(request, header_parameters, body_content, stream=False, **operation_config)
+
+ if response.status_code not in [200]:
+ exp = CloudError(response)
+ exp.request_id = response.headers.get('x-ms-request-id')
+ raise exp
+
+ deserialized = None
+ header_dict = {}
+
+ if response.status_code == 200:
+ deserialized = self._deserialize('object', response)
+ header_dict = {
+ 'x-ms-command-statuscode': 'int',
+ 'x-ms-request-id': 'str',
+ }
+
+ if raw:
+ client_raw_response = ClientRawResponse(deserialized, response)
+ client_raw_response.add_headers(header_dict)
+ return client_raw_response
+
+ return deserialized
+ invoke_interface_command.metadata = {'url': '/digitalTwins/{digitalTwinId}/interfaces/{interfaceName}/commands/{commandName}'}
diff --git a/azext_iot/service_sdk/models/__init__.py b/azext_iot/service_sdk/models/__init__.py
index fbbb78115..aa0335a88 100644
--- a/azext_iot/service_sdk/models/__init__.py
+++ b/azext_iot/service_sdk/models/__init__.py
@@ -38,6 +38,16 @@
from .job_response import JobResponse
from .module import Module
from .cloud_to_device_method_result import CloudToDeviceMethodResult
+from .desired import Desired
+from .desired_state import DesiredState
+from .digital_twin_interfaces import DigitalTwinInterfaces
+from .digital_twin_interfaces_patch import DigitalTwinInterfacesPatch
+from .digital_twin_interfaces_patch_interfaces_value import DigitalTwinInterfacesPatchInterfacesValue
+from .digital_twin_interfaces_patch_interfaces_value_properties_value import DigitalTwinInterfacesPatchInterfacesValuePropertiesValue
+from .digital_twin_interfaces_patch_interfaces_value_properties_value_desired import DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired
+from .interface import Interface
+from .property import Property
+from .reported import Reported
__all__ = [
'ConfigurationMetrics',
@@ -69,4 +79,14 @@
'JobResponse',
'Module',
'CloudToDeviceMethodResult',
+ 'Desired',
+ 'DigitalTwinInterfaces',
+ 'DigitalTwinInterfacesPatch',
+ 'DigitalTwinInterfacesPatchInterfacesValue',
+ 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValue',
+ 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired',
+ 'DesiredState',
+ 'Interface',
+ 'Property',
+ 'Reported',
]
diff --git a/azext_iot/service_sdk/models/desired.py b/azext_iot/service_sdk/models/desired.py
new file mode 100644
index 000000000..8ac77f637
--- /dev/null
+++ b/azext_iot/service_sdk/models/desired.py
@@ -0,0 +1,29 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class Desired(Model):
+ """Desired.
+
+ :param value: The desired value of the interface property to set in a
+ digitalTwin.
+ :type value: object
+ """
+
+ _attribute_map = {
+ 'value': {'key': 'value', 'type': 'object'},
+ }
+
+ def __init__(self, value=None):
+ super(Desired, self).__init__()
+ self.value = value
diff --git a/azext_iot/service_sdk/models/desired_state.py b/azext_iot/service_sdk/models/desired_state.py
new file mode 100644
index 000000000..5c29a0445
--- /dev/null
+++ b/azext_iot/service_sdk/models/desired_state.py
@@ -0,0 +1,40 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class DesiredState(Model):
+ """DesiredState.
+
+ :param code: Status code for the operation.
+ :type code: int
+ :param sub_code: Sub status code for the status.
+ :type sub_code: int
+ :param version: Version of the desired value received.
+ :type version: long
+ :param description: Description of the status.
+ :type description: str
+ """
+
+ _attribute_map = {
+ 'code': {'key': 'code', 'type': 'int'},
+ 'sub_code': {'key': 'subCode', 'type': 'int'},
+ 'version': {'key': 'version', 'type': 'long'},
+ 'description': {'key': 'description', 'type': 'str'},
+ }
+
+ def __init__(self, code=None, sub_code=None, version=None, description=None):
+ super(DesiredState, self).__init__()
+ self.code = code
+ self.sub_code = sub_code
+ self.version = version
+ self.description = description
diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces.py b/azext_iot/service_sdk/models/digital_twin_interfaces.py
new file mode 100644
index 000000000..cac98248a
--- /dev/null
+++ b/azext_iot/service_sdk/models/digital_twin_interfaces.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class DigitalTwinInterfaces(Model):
+ """DigitalTwinInterfaces.
+
+ :param interfaces: Interface(s) data on the digital twin.
+ :type interfaces: dict[str, ~service.models.Interface]
+ :param version: Version of digital twin.
+ :type version: long
+ """
+
+ _attribute_map = {
+ 'interfaces': {'key': 'interfaces', 'type': '{Interface}'},
+ 'version': {'key': 'version', 'type': 'long'},
+ }
+
+ def __init__(self, interfaces=None, version=None):
+ super(DigitalTwinInterfaces, self).__init__()
+ self.interfaces = interfaces
+ self.version = version
diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch.py
new file mode 100644
index 000000000..512ff1729
--- /dev/null
+++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch.py
@@ -0,0 +1,28 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class DigitalTwinInterfacesPatch(Model):
+ """DigitalTwinInterfacesPatch.
+
+ :param interfaces: Interface(s) data to patch in the digital twin.
+ :type interfaces: dict[str, ~service.models.DigitalTwinInterfacesPatchInterfacesValue]
+ """
+
+ _attribute_map = {
+ 'interfaces': {'key': 'interfaces', 'type': '{DigitalTwinInterfacesPatchInterfacesValue}'},
+ }
+
+ def __init__(self, interfaces=None):
+ super(DigitalTwinInterfacesPatch, self).__init__()
+ self.interfaces = interfaces
diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py
new file mode 100644
index 000000000..1ce39941b
--- /dev/null
+++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value.py
@@ -0,0 +1,28 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class DigitalTwinInterfacesPatchInterfacesValue(Model):
+ """DigitalTwinInterfacesPatchInterfacesValue.
+
+ :param properties: List of properties to update in an interface.
+ :type properties: dict[str, ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValue]
+ """
+
+ _attribute_map = {
+ 'properties': {'key': 'properties', 'type': '{DigitalTwinInterfacesPatchInterfacesValuePropertiesValue}'},
+ }
+
+ def __init__(self, properties=None):
+ super(DigitalTwinInterfacesPatchInterfacesValue, self).__init__()
+ self.properties = properties
diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py
new file mode 100644
index 000000000..c60a512bd
--- /dev/null
+++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value.py
@@ -0,0 +1,28 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class DigitalTwinInterfacesPatchInterfacesValuePropertiesValue(Model):
+ """DigitalTwinInterfacesPatchInterfacesValuePropertiesValue.
+
+ :param desired:
+ :type desired: ~service.models.DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired
+ """
+
+ _attribute_map = {
+ 'desired': {'key': 'desired', 'type': 'DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired'},
+ }
+
+ def __init__(self, desired=None):
+ super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValue, self).__init__()
+ self.desired = desired
diff --git a/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py
new file mode 100644
index 000000000..1e90b4098
--- /dev/null
+++ b/azext_iot/service_sdk/models/digital_twin_interfaces_patch_interfaces_value_properties_value_desired.py
@@ -0,0 +1,29 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired(Model):
+ """DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired.
+
+ :param value: The desired value of the interface property to set in a
+ digitalTwin.
+ :type value: object
+ """
+
+ _attribute_map = {
+ 'value': {'key': 'value', 'type': 'object'},
+ }
+
+ def __init__(self, value=None):
+ super(DigitalTwinInterfacesPatchInterfacesValuePropertiesValueDesired, self).__init__()
+ self.value = value
diff --git a/azext_iot/service_sdk/models/interface.py b/azext_iot/service_sdk/models/interface.py
new file mode 100644
index 000000000..b81ccd6cc
--- /dev/null
+++ b/azext_iot/service_sdk/models/interface.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class Interface(Model):
+ """Interface.
+
+ :param name: Full name of digital twin interface.
+ :type name: str
+ :param properties: List of all properties in an interface.
+ :type properties: dict[str, ~service.models.Property]
+ """
+
+ _attribute_map = {
+ 'name': {'key': 'name', 'type': 'str'},
+ 'properties': {'key': 'properties', 'type': '{Property}'},
+ }
+
+ def __init__(self, name=None, properties=None):
+ super(Interface, self).__init__()
+ self.name = name
+ self.properties = properties
diff --git a/azext_iot/service_sdk/models/property.py b/azext_iot/service_sdk/models/property.py
new file mode 100644
index 000000000..c950c11db
--- /dev/null
+++ b/azext_iot/service_sdk/models/property.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class Property(Model):
+ """Property.
+
+ :param reported:
+ :type reported: ~service.models.Reported
+ :param desired:
+ :type desired: ~service.models.Desired
+ """
+
+ _attribute_map = {
+ 'reported': {'key': 'reported', 'type': 'Reported'},
+ 'desired': {'key': 'desired', 'type': 'Desired'},
+ }
+
+ def __init__(self, reported=None, desired=None):
+ super(Property, self).__init__()
+ self.reported = reported
+ self.desired = desired
\ No newline at end of file
diff --git a/azext_iot/service_sdk/models/reported.py b/azext_iot/service_sdk/models/reported.py
new file mode 100644
index 000000000..1dc58b366
--- /dev/null
+++ b/azext_iot/service_sdk/models/reported.py
@@ -0,0 +1,32 @@
+# coding=utf-8
+# --------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for
+# license information.
+#
+# Code generated by Microsoft (R) AutoRest Code Generator.
+# Changes may cause incorrect behavior and will be lost if the code is
+# regenerated.
+# --------------------------------------------------------------------------
+
+from msrest.serialization import Model
+
+
+class Reported(Model):
+ """Reported.
+
+ :param value: The current interface property value in a digitalTwin.
+ :type value: object
+ :param desired_state:
+ :type desired_state: ~service.models.DesiredState
+ """
+
+ _attribute_map = {
+ 'value': {'key': 'value', 'type': 'object'},
+ 'desired_state': {'key': 'desiredState', 'type': 'DesiredState'},
+ }
+
+ def __init__(self, value=None, desired_state=None):
+ super(Reported, self).__init__()
+ self.value = value
+ self.desired_state = desired_state
diff --git a/azext_iot/service_sdk/version.py b/azext_iot/service_sdk/version.py
index 9a6687264..729e580c0 100644
--- a/azext_iot/service_sdk/version.py
+++ b/azext_iot/service_sdk/version.py
@@ -9,5 +9,4 @@
# regenerated.
# --------------------------------------------------------------------------
-VERSION = "2018-08-30-preview"
-
+VERSION = "2019-07-01-preview"
diff --git a/azext_iot/tests/test_device_digitaltwin_interfaces.json b/azext_iot/tests/test_device_digitaltwin_interfaces.json
new file mode 100644
index 000000000..22cf08ae5
--- /dev/null
+++ b/azext_iot/tests/test_device_digitaltwin_interfaces.json
@@ -0,0 +1,58 @@
+{
+ "interfaces": {
+ "environmentalSensor": {
+ "name": "environmentalSensor",
+ "properties": {
+ "brightness": {
+ "desired": {
+ "value": 123
+ },
+ "reported": {
+ "desiredState": {
+ "code": 200,
+ "description": "Brightness updated",
+ "version": 4
+ },
+ "value": 123
+ }
+ },
+ "name": {
+ "desired": {
+ "value": "test"
+ },
+ "reported": {
+ "desiredState": {
+ "code": 200,
+ "description": "Property Updated Successfully",
+ "version": 4
+ },
+ "value": "test"
+ }
+ },
+ "state": {
+ "reported": {
+ "value": true
+ }
+ }
+ }
+ },
+ "urn_azureiot_ModelDiscovery_DigitalTwin": {
+ "name": "urn_azureiot_ModelDiscovery_DigitalTwin",
+ "properties": {
+ "modelInformation": {
+ "reported": {
+ "value": {
+ "interfaces": {
+ "environmentalSensor": "urn:contoso:com:EnvironmentalSensor:1",
+ "urn_azureiot_ModelDiscovery_DigitalTwin": "urn:azureiot:ModelDiscovery:DigitalTwin:1",
+ "urn_azureiot_ModelDiscovery_ModelInformation": "urn:azureiot:ModelDiscovery:ModelInformation:1"
+ },
+ "modelId": "urn:azureiot:testdevicecapabilitymodel:1"
+ }
+ }
+ }
+ }
+ }
+ },
+ "version": 1
+ }
\ No newline at end of file
diff --git a/azext_iot/tests/test_device_digitaltwin_invoke_command.json b/azext_iot/tests/test_device_digitaltwin_invoke_command.json
new file mode 100644
index 000000000..fe34ec6b9
--- /dev/null
+++ b/azext_iot/tests/test_device_digitaltwin_invoke_command.json
@@ -0,0 +1,5 @@
+{
+ "blinkRequest": {
+ "interval": 1
+ }
+}
\ No newline at end of file
diff --git a/azext_iot/tests/test_device_digitaltwin_property_update.json b/azext_iot/tests/test_device_digitaltwin_property_update.json
new file mode 100644
index 000000000..09bcbd6b4
--- /dev/null
+++ b/azext_iot/tests/test_device_digitaltwin_property_update.json
@@ -0,0 +1,11 @@
+{
+ "environmentalSensor": {
+ "properties": {
+ "name": {
+ "desired": {
+ "value": "test-update"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/azext_iot/tests/test_iot_digitaltwin_unit.py b/azext_iot/tests/test_iot_digitaltwin_unit.py
new file mode 100644
index 000000000..a662bed64
--- /dev/null
+++ b/azext_iot/tests/test_iot_digitaltwin_unit.py
@@ -0,0 +1,417 @@
+# coding=utf-8
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for license information.
+# --------------------------------------------------------------------------------------------
+# pylint: disable=W0613,W0621
+
+import pytest
+import random
+import json
+import os
+import sys
+
+from uuid import uuid4
+from azext_iot.operations import digitaltwin as subject
+from azext_iot.operations.digitaltwin import INTERFACE_KEY_NAME
+from azext_iot.common.utility import url_encode_str, validate_min_python_version
+from knack.util import CLIError
+from azure.cli.core.util import read_file_content
+
+# Path hack.
+sys.path.insert(0, os.path.abspath('.'))
+
+_device_digitaltwin_invoke_command_payload = 'test_device_digitaltwin_invoke_command.json'
+_device_digitaltwin_payload_file = 'test_device_digitaltwin_interfaces.json'
+_device_digitaltwin_property_update_payload_file = 'test_device_digitaltwin_property_update.json'
+_pnp_show_interface_file = 'test_pnp_interface_show.json'
+_pnp_list_interface_file = 'test_pnp_interface_list.json'
+path_iot_hub_monitor_events = 'azext_iot.operations.digitaltwin._iot_hub_monitor_events'
+device_id = 'mydevice'
+hub_entity = 'myhub.azure-devices.net'
+
+# Patch Paths #
+path_mqtt_client = 'azext_iot.operations._mqtt.mqtt.Client'
+path_service_client = 'msrest.service_client.ServiceClient.send'
+path_ghcs = 'azext_iot.operations.hub.get_iot_hub_connection_string'
+path_iot_hub_service_factory = 'azext_iot.common._azure.iot_hub_service_factory'
+path_sas = 'azext_iot._factory.SasTokenAuthentication'
+
+mock_target = {}
+mock_target['entity'] = hub_entity
+mock_target['primarykey'] = 'rJx/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo='
+mock_target['secondarykey'] = 'aCd/6rJ6rmG4ak890+eW5MYGH+A0uzRvjGNjg3Ve8sfo='
+mock_target['policy'] = 'iothubowner'
+mock_target['subscription'] = "5952cff8-bcd1-4235-9554-af2c0348bf23"
+mock_target['location'] = "westus2"
+mock_target['sku_tier'] = "Standard"
+generic_cs_template = 'HostName={};SharedAccessKeyName={};SharedAccessKey={}'
+
+
+def generate_cs(hub=hub_entity, policy=mock_target['policy'], key=mock_target['primarykey']):
+ return generic_cs_template.format(hub, policy, key)
+
+
+mock_target['cs'] = generate_cs()
+
+
+def change_dir():
+ from inspect import getsourcefile
+ os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0))))
+
+
+def generate_device_interfaces_payload():
+ change_dir()
+ return json.loads(read_file_content(_device_digitaltwin_payload_file))
+
+
+def generate_pnp_interface_show_payload():
+ change_dir()
+ return json.loads(read_file_content(_pnp_show_interface_file))
+
+
+def generate_pnp_interface_list_payload():
+ change_dir()
+ return json.loads(read_file_content(_pnp_list_interface_file))
+
+
+def generate_device_digitaltwin_property_update_payload(content_from_file=False):
+ change_dir()
+ if content_from_file:
+ return (None, _device_digitaltwin_property_update_payload_file)
+
+ return (str(read_file_content(_device_digitaltwin_property_update_payload_file)),
+ _device_digitaltwin_property_update_payload_file)
+
+
+def generate_device_digitaltwin_invoke_command_payload(content_from_file=False):
+ change_dir()
+ if content_from_file:
+ return (None, _device_digitaltwin_invoke_command_payload)
+
+ return (str(read_file_content(_device_digitaltwin_invoke_command_payload)),
+ _device_digitaltwin_invoke_command_payload)
+
+
+@pytest.fixture()
+def fixture_cmd(mocker):
+ # Placeholder for later use
+ mocker.patch(path_iot_hub_service_factory)
+ cmd = mocker.MagicMock(name='cli cmd context')
+ return cmd
+
+
+@pytest.fixture()
+def fixture_ghcs(mocker):
+ ghcs = mocker.patch(path_ghcs)
+ ghcs.return_value = mock_target
+ return ghcs
+
+
+@pytest.fixture(params=[400, 401, 500])
+def serviceclient_generic_error(mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {'error': 'something failed'})
+ return service_client
+
+
+@pytest.fixture()
+def fixture_monitor_events(mocker):
+ mocker.patch(path_iot_hub_monitor_events)
+
+
+def build_mock_response(mocker, status_code=200, payload=None, headers=None, raw=False):
+ response = mocker.MagicMock(name='response')
+ response.status_code = status_code
+ del response.context
+ del response._attribute_map
+
+ if raw:
+ response.text = payload
+ else:
+ response.text.return_value = json.dumps(payload)
+
+ if headers:
+ response.headers = headers
+ return response
+
+
+class TestDTInterfaceList(object):
+
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ service_client.return_value = build_mock_response(mocker, request.param, output)
+ return service_client
+
+ def test_iot_digitaltwin_interface_list(self, fixture_cmd, serviceclient):
+ result = subject.iot_digitaltwin_interface_list(fixture_cmd, device_id=device_id,
+ login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+
+ assert method == 'GET'
+ assert '{}/digitalTwins/{}/interfaces/{}?'.format(mock_target['entity'], device_id,
+ INTERFACE_KEY_NAME) in url
+ assert json.dumps(result)
+ assert len(result['interfaces']) == 3
+
+ @pytest.mark.parametrize("exp", [(CLIError)])
+ def test_iot_digitaltwin_interface_list_error(self, fixture_cmd, serviceclient_generic_error, exp):
+ with pytest.raises(exp):
+ subject.iot_digitaltwin_interface_list(fixture_cmd, device_id=device_id,
+ login=mock_target['cs'])
+
+
+class TestDTCommandList(object):
+
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ payload_list = generate_pnp_interface_list_payload()
+ payload_show = generate_pnp_interface_show_payload()
+ test_side_effect = [
+ build_mock_response(mocker, request.param, payload=output),
+ build_mock_response(mocker, request.param, payload=payload_list),
+ build_mock_response(mocker, request.param, payload=payload_show)
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ def test_iot_digitaltwin_command_list(self, fixture_cmd, serviceclient):
+ result = subject.iot_digitaltwin_command_list(fixture_cmd, device_id=device_id, source_model='public',
+ interface='environmentalSensor', login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+
+ assert method == 'GET'
+ assert '/models/' in url
+ assert json.dumps(result)
+ assert len(result['interfaces']) == 1
+ assert result['interfaces'][0]['name'] == 'environmentalSensor'
+ assert len(result['interfaces'][0]['commands']) == 3
+
+ @pytest.mark.parametrize("exp", [(CLIError)])
+ def test_iot_digitaltwin_command_list_error(self, fixture_cmd, serviceclient_generic_error, exp):
+ with pytest.raises(exp):
+ subject.iot_digitaltwin_command_list(fixture_cmd, device_id=device_id, source_model='public',
+ login=mock_target['cs'])
+
+ @pytest.mark.parametrize("interface, exp", [('inter1', CLIError)])
+ def test_iot_digitaltwin_command_list_args_error(self, fixture_cmd, serviceclient, interface, exp):
+ with pytest.raises(exp):
+ subject.iot_digitaltwin_command_list(fixture_cmd, device_id=device_id, interface=interface,
+ login=mock_target['cs'], source_model='public')
+
+
+class TestDTPropertiesList(object):
+
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ payload_list = generate_pnp_interface_list_payload()
+ payload_show = generate_pnp_interface_show_payload()
+ test_side_effect = [
+ build_mock_response(mocker, request.param, payload=output),
+ build_mock_response(mocker, request.param, payload=payload_list),
+ build_mock_response(mocker, request.param, payload=payload_show)
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("target_interface", [('environmentalSensor')])
+ def test_iot_digitaltwin_properties_list(self, fixture_cmd, serviceclient, target_interface):
+ result = subject.iot_digitaltwin_properties_list(fixture_cmd, device_id=device_id, source_model='public',
+ interface=target_interface, login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+
+ assert method == 'GET'
+ assert '/models/' in url
+ assert json.dumps(result)
+ if target_interface:
+ assert len(result['interfaces']) == 1
+ assert result['interfaces'][0]['name'] == 'environmentalSensor'
+ assert len(result['interfaces'][0]['properties']) == 3
+
+ @pytest.mark.parametrize("exp", [(CLIError)])
+ def test_iot_digitaltwin_properties_list_error(self, fixture_cmd, serviceclient_generic_error, exp):
+ with pytest.raises(exp):
+ subject.iot_digitaltwin_properties_list(fixture_cmd, device_id=device_id, source_model='public',
+ login=mock_target['cs'])
+
+ @pytest.mark.parametrize("interface, exp", [('inter1', CLIError)])
+ def test_iot_digitaltwin_properties_list_args_error(self, fixture_cmd, serviceclient, interface, exp):
+ with pytest.raises(exp):
+ subject.iot_digitaltwin_properties_list(fixture_cmd, device_id=device_id, interface=interface,
+ login=mock_target['cs'], source_model='public')
+
+
+class TestDTPropertyUpdate(object):
+
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("payload_scenario", [
+ (generate_device_digitaltwin_property_update_payload()),
+ (generate_device_digitaltwin_property_update_payload(content_from_file=True))])
+ def test_iot_digitaltwin_property_update(self, fixture_cmd, serviceclient, payload_scenario):
+
+ payload = None
+
+ # If file path provided
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_device_digitaltwin_property_update_payload_file))
+
+ subject.iot_digitaltwin_property_update(fixture_cmd, device_id=device_id,
+ interface_payload=payload, login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+
+ assert method == 'PATCH'
+ assert '{}/digitalTwins/{}/interfaces?'.format(mock_target['entity'], device_id) in url
+
+ @pytest.mark.parametrize("payload_scenario", [(generate_device_digitaltwin_property_update_payload())])
+ def test_iot_digitaltwin_property_update_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario):
+ with pytest.raises(CLIError):
+ subject.iot_digitaltwin_property_update(fixture_cmd, device_id=device_id,
+ interface_payload=payload_scenario[0],
+ login=mock_target['cs'])
+
+
+class TestDTInvokeCommand(object):
+
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ payload_list = generate_pnp_interface_list_payload()
+ payload_show = generate_pnp_interface_show_payload()
+ test_side_effect = [
+ build_mock_response(mocker, request.param, payload=output),
+ build_mock_response(mocker, request.param, payload=payload_list),
+ build_mock_response(mocker, request.param, payload=payload_show),
+ build_mock_response(mocker, request.param, {})
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("command_payload", [
+ (generate_device_digitaltwin_invoke_command_payload()),
+ (generate_device_digitaltwin_invoke_command_payload(content_from_file=True))])
+ def test_iot_digitaltwin_invoke_command(self, fixture_cmd, serviceclient, command_payload):
+
+ payload = None
+ interface = 'environmentalSensor'
+ command = 'blink'
+
+ # If file path provided
+ if not command_payload[0]:
+ payload = command_payload[1]
+ else:
+ payload = str(read_file_content(_device_digitaltwin_invoke_command_payload))
+
+ subject.iot_digitaltwin_invoke_command(fixture_cmd, device_id=device_id,
+ interface=interface,
+ command_name=command,
+ command_payload=payload,
+ login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+
+ assert method == 'POST'
+ assert '{}/digitalTwins/{}/interfaces/{}/commands/{}?'.format(mock_target['entity'], device_id,
+ interface, command) in url
+
+ @pytest.fixture(params=[(200, 400), (200, 401), (200, 500)])
+ def serviceclient_error(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ test_side_effect = [
+ build_mock_response(mocker, request.param[0], payload=output),
+ build_mock_response(mocker, request.param[1], {})
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("command_payload, interface, command", [
+ (generate_device_digitaltwin_invoke_command_payload(), 'environmentalSensor', 'blink'),
+ (generate_device_digitaltwin_invoke_command_payload(content_from_file=True), 'environmentalSensor', 'blink'),
+ (generate_device_digitaltwin_invoke_command_payload(), 'test', 'blink'),
+ (generate_device_digitaltwin_invoke_command_payload(), 'environmentalSensor', 'test')])
+ def test_iot_digitaltwin_property_update_error(self, fixture_cmd, serviceclient_error,
+ command_payload, interface, command):
+ payload = None
+ if not command_payload[0]:
+ payload = command_payload[1]
+ else:
+ payload = str(read_file_content(_device_digitaltwin_invoke_command_payload))
+ with pytest.raises(CLIError):
+ subject.iot_digitaltwin_invoke_command(fixture_cmd, device_id=device_id,
+ interface=interface,
+ command_name=command,
+ command_payload=payload,
+ login=mock_target['cs'])
+
+
+@pytest.mark.skipif(not validate_min_python_version(3, 5, exit_on_fail=False), reason="minimum python version not satisfied")
+class TestDTMonitorEvents(object):
+
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, fixture_monitor_events, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ payload_list = generate_pnp_interface_list_payload()
+ payload_show = generate_pnp_interface_show_payload()
+ test_side_effect = [
+ build_mock_response(mocker, request.param, payload=output),
+ build_mock_response(mocker, request.param, payload=payload_list),
+ build_mock_response(mocker, request.param, payload=payload_show)
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ def test_iot_digitaltwin_monitor_events(self, fixture_cmd, serviceclient):
+ subject.iot_digitaltwin_monitor_events(fixture_cmd, device_id=device_id,
+ interface='environmentalSensor', source_model='public',
+ yes=True, properties='all', login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+
+ assert method == 'GET'
+ assert '/models/' in url
+
+ @pytest.fixture(params=[200])
+ def serviceclienterror(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ output = generate_device_interfaces_payload()
+ payload_list = generate_pnp_interface_list_payload()
+ payload_show = generate_pnp_interface_show_payload()
+ test_side_effect = [
+ build_mock_response(mocker, request.param, payload=output),
+ build_mock_response(mocker, request.param, payload=payload_list),
+ build_mock_response(mocker, request.param, payload=payload_show)
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("interface, timeout", [('inter1', 0), ('environmentalSensor', -1)])
+ def test_iot_digitaltwin_monitor_events_error(self, fixture_cmd, serviceclienterror, interface, timeout):
+ with pytest.raises(CLIError):
+ subject.iot_digitaltwin_monitor_events(fixture_cmd, device_id=device_id, source_model='public',
+ interface=interface, timeout=timeout,
+ yes=True, properties='all', login=mock_target['cs'])
diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py
index 608ff5b90..f88e07d0a 100644
--- a/azext_iot/tests/test_iot_ext_int.py
+++ b/azext_iot/tests/test_iot_ext_int.py
@@ -1336,8 +1336,8 @@ def test_hub_monitor_events(self):
LIVE_HUB, LIVE_RG, LIVE_CONSUMER_GROUPS[0], enqueued_time), ['{\\r\\n\\"payload_data1\\"\\"payload_value1\\"\\r\\n}'])
for cg in LIVE_CONSUMER_GROUPS:
- self.cmd('az iot hub consumer-group delete --hub-name {} --resource-group {} --name {}'.format(LIVE_HUB, LIVE_RG, cg),
- expect_failure=False)
+ self.cmd('az iot hub consumer-group delete --hub-name {} --resource-group {} --name {}'
+ .format(LIVE_HUB, LIVE_RG, cg), expect_failure=False)
@pytest.mark.skipif(not validate_min_python_version(3, 4, exit_on_fail=False), reason="minimum python version not satisfied")
def test_hub_monitor_feedback(self):
diff --git a/azext_iot/tests/test_iot_pnp_int.py b/azext_iot/tests/test_iot_pnp_int.py
new file mode 100644
index 000000000..6ab827e2c
--- /dev/null
+++ b/azext_iot/tests/test_iot_pnp_int.py
@@ -0,0 +1,174 @@
+# coding=utf-8
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Unpublished works.
+# --------------------------------------------------------------------------------------------
+import pytest
+import random
+import json
+import os
+import sys
+
+from os.path import exists
+from uuid import uuid4
+from azure.cli.testsdk import LiveScenarioTest
+from azure.cli.core.util import read_file_content
+
+
+# Add test tools to path
+sys.path.append(os.path.abspath(os.path.join('.', 'iotext_test_tools')))
+
+# Set these to the proper PnP Endpoint, PnP Cstring and PnP Repository for Live Integration Tests.
+_endpoint = os.environ.get('azext_pnp_endpoint')
+_repo_id = os.environ.get('azext_pnp_repository')
+_repo_cs = os.environ.get('azext_pnp_cs')
+
+_interface_payload = 'test_pnp_create_payload_interface.json'
+_capability_model_payload = 'test_pnp_create_payload_model.json'
+
+if not all([_endpoint, _repo_id, _repo_cs]):
+ raise ValueError('Set azext_pnp_endpoint, azext_pnp_repository and azext_pnp_cs to run PnP model integration tests.')
+
+
+def change_dir():
+ from inspect import getsourcefile
+ os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0))))
+
+
+class TestPnPModel(LiveScenarioTest):
+
+ rand_val = random.randint(1, 10001)
+
+ def __init__(self, _):
+ from iotext_test_tools import DummyCliOutputProducer
+ super(TestPnPModel, self).__init__(_)
+ self.cli_ctx = DummyCliOutputProducer()
+ self.kwargs.update({
+ 'endpoint': _endpoint,
+ 'repo': _repo_id,
+ 'repo_cs': _repo_cs,
+ 'interface': 'test_interface_definition.json',
+ 'interface-updated': 'test_interface_updated_definition.json',
+ 'model': 'test_model_definition.json',
+ 'model-updated': 'test_model_updated_definition.json'
+ })
+
+ def setUp(self):
+ change_dir()
+ if(self._testMethodName == 'test_interface_life_cycle'):
+ interface = str(read_file_content(_interface_payload))
+ _interface_id = '{}{}'.format(json.loads(interface)['@id'], TestPnPModel.rand_val)
+ self.kwargs.update({'interface_id': _interface_id})
+ interface_newContent = interface.replace(json.loads(interface)['@id'], self.kwargs['interface_id'])
+ interface_newContent = interface_newContent.replace('\n', '')
+
+ fo = open(self.kwargs['interface'], "w+", encoding='utf-8')
+ fo.write(interface_newContent)
+ fo.close()
+
+ if(self._testMethodName == 'test_model_life_cycle'):
+ model = str(read_file_content(_capability_model_payload))
+ _model_id = '{}{}'.format(json.loads(model)['@id'], TestPnPModel.rand_val)
+ self.kwargs.update({'model_id': _model_id})
+ model_newContent = model.replace(json.loads(model)['@id'], self.kwargs['model_id'])
+ model_newContent = model_newContent.replace('\n', '')
+
+ fo = open(self.kwargs['model'], "w+", encoding='utf-8')
+ fo.write(model_newContent)
+ fo.close()
+
+ def tearDown(self):
+ change_dir()
+ if exists(self.kwargs['interface-updated']):
+ os.remove(self.kwargs['interface-updated'])
+ if exists(self.kwargs['model-updated']):
+ os.remove(self.kwargs['model-updated'])
+ if exists(self.kwargs['interface']):
+ os.remove(self.kwargs['interface'])
+ if exists(self.kwargs['model']):
+ os.remove(self.kwargs['model'])
+
+ def test_interface_life_cycle(self):
+
+ # Error: missing repo-id or login
+ self.cmd('iot pnp interface create -e {endpoint} --def {interface}', expect_failure=True)
+
+ # Error: Invalid Interface definition file
+ self.cmd('iot pnp interface create -e {endpoint} -r {repo} --def interface', expect_failure=True)
+
+ # Error: wrong path of Interface definition
+ self.cmd('iot pnp interface create -e {endpoint} -r {repo} --def interface.json', expect_failure=True)
+
+ # Success: Create new Interface
+ self.cmd('iot pnp interface create -e {endpoint} -r {repo} --def {interface}', checks=self.is_empty())
+
+ # Checking the Interface list
+ self.cmd('iot pnp interface list -e {endpoint} -r {repo}',
+ checks=[self.greater_than('length([*])', 0)])
+
+ # Get Interface
+ interface = self.cmd('iot pnp interface show -e {endpoint} -r {repo} -i {interface_id}').get_output_in_json()
+ assert json.dumps(interface)
+ assert interface['@id'] == self.kwargs['interface_id']
+ assert interface['displayName'] == 'MXChip1'
+ assert len(interface['contents']) > 0
+
+ # Success: Update Interface
+ interface = str(read_file_content(self.kwargs['interface']))
+ display_name = json.loads(interface)['displayName']
+ interface_newContent = interface.replace(display_name, '{}-Updated'.format(display_name))
+ interface_newContent = interface_newContent.replace('\n', '')
+ fo = open(self.kwargs['interface-updated'], "w+", encoding='utf-8')
+ fo.write(interface_newContent)
+ fo.close()
+ self.cmd('iot pnp interface update -e {endpoint} -r {repo} --def {interface-updated}', checks=self.is_empty())
+
+ # Todo: Publish Interface
+ self.cmd('iot pnp interface publish -e {endpoint} -r {repo} -i {interface_id}', checks=self.is_empty())
+
+ # Success: Delete Interface
+ self.cmd('iot pnp interface delete -e {endpoint} -r {repo} -i {interface_id}', checks=self.is_empty())
+
+ def test_model_life_cycle(self):
+
+ # Checking the Capability-Model list
+ self.cmd('iot pnp capability-model list -e {endpoint} -r {repo}',
+ checks=[self.check('length([*])', 0)])
+
+ # Error: missing repo-id or login
+ self.cmd('iot pnp capability-model create -e {endpoint} --def {model}', expect_failure=True)
+
+ # Error: Invalid Capability-Model definition file
+ self.cmd('iot pnp capability-model create -e {endpoint} -r {repo} --def model', expect_failure=True)
+
+ # Error: wrong path of Capability-Model definition
+ self.cmd('iot pnp capability-model create -e {endpoint} -r {repo} --def model.json', expect_failure=True)
+
+ # Success: Create new Capability-Model
+ self.cmd('iot pnp capability-model create -e {endpoint} -r {repo} --def {model}', checks=self.is_empty())
+
+ # Checking the Capability-Model list
+ self.cmd('iot pnp capability-model list -e {endpoint} -r {repo}',
+ checks=[self.check('length([*])', 1)])
+
+ # Get Capability-Model
+ model = self.cmd('iot pnp capability-model show -e {endpoint} -r {repo} -m {model_id}').get_output_in_json()
+ assert json.dumps(model)
+ assert model['@id'] == self.kwargs['model_id']
+ assert len(model['implements']) > 0
+
+ # Success: Update Capability-Model
+ model = str(read_file_content(self.kwargs['model']))
+ display_name = json.loads(model)['displayName']
+ model_newContent = model.replace(display_name, '{}-Updated'.format(display_name))
+ model_newContent = model_newContent.replace('\n', '')
+ fo = open(self.kwargs['model-updated'], "w+", encoding='utf-8')
+ fo.write(model_newContent)
+ fo.close()
+ self.cmd('iot pnp capability-model update -e {endpoint} -r {repo} --def {model-updated}', checks=self.is_empty())
+
+ # Todo: Publish Capability-Model
+ self.cmd('iot pnp capability-model publish -e {endpoint} -r {repo} -m {model_id}', checks=self.is_empty())
+
+ # Success: Delete Capability-Model
+ self.cmd('iot pnp capability-model delete -e {endpoint} -r {repo} -m {model_id}', checks=self.is_empty())
diff --git a/azext_iot/tests/test_iot_pnp_unit.py b/azext_iot/tests/test_iot_pnp_unit.py
new file mode 100644
index 000000000..5f6fd08da
--- /dev/null
+++ b/azext_iot/tests/test_iot_pnp_unit.py
@@ -0,0 +1,791 @@
+# coding=utf-8
+# --------------------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Unpublished works.
+# --------------------------------------------------------------------------------------------
+# pylint: disable=W0613,W0621
+# flake8: noqa
+
+
+import pytest
+import random
+import json
+import os
+import sys
+
+from uuid import uuid4
+from azext_iot.operations import pnp as subject
+from azext_iot.common.utility import url_encode_str
+from knack.util import CLIError
+from azure.cli.core.util import read_file_content
+
+
+# Path hack.
+sys.path.insert(0, os.path.abspath('.'))
+
+from test_iot_ext_unit import (fixture_cmd,
+ fixture_sas,
+ path_service_client,
+ serviceclient_generic_error,
+ build_mock_response)
+
+_repo_endpoint = 'https://{}.{}'.format(str(uuid4()), 'com')
+_repo_id = str(uuid4()).replace('-', '')
+_repo_keyname = str(uuid4()).replace('-', '')
+_repo_secret = 'lMT+wSy8TIzDASRMlhxwpYxG3mWba45YqCFUo6Qngju5uZS9V4tM2yh5pn3zdB0FC3yRx91UnSWjdr/jLutPbg=='
+generic_cs_template = 'HostName={};RepositoryId={};SharedAccessKeyName={};SharedAccessKey={}'
+path_ghcs = 'azext_iot.operations.pnp.get_iot_pnp_connection_string'
+_pnp_create_interface_payload_file = 'test_pnp_create_payload_interface.json'
+_pnp_create_model_payload_file = 'test_pnp_create_payload_model.json'
+_pnp_show_interface_file = 'test_pnp_interface_show.json'
+_pnp_generic_interface_id = 'urn:example:interfaces:MXChip:1'
+_pnp_generic_model_id = 'urn:example:capabilityModels:Mxchip:1'
+
+@pytest.fixture()
+def fixture_ghcs(mocker):
+ ghcs = mocker.patch(path_ghcs)
+ ghcs.return_value = mock_target
+ return ghcs
+
+def generate_cs(endpoint=_repo_endpoint, repository=_repo_id, policy=_repo_keyname, key=_repo_secret):
+ return generic_cs_template.format(endpoint, repository, policy, key)
+
+def change_dir():
+ from inspect import getsourcefile
+ os.chdir(os.path.dirname(os.path.abspath(getsourcefile(lambda: 0))))
+
+mock_target = {}
+mock_target['cs'] = generate_cs()
+mock_target['policy'] = _repo_keyname
+mock_target['primarykey'] = _repo_secret
+mock_target['repository_id'] = _repo_id
+mock_target['entity'] = _repo_endpoint
+mock_target['entity'] = mock_target['entity'].replace('https://', '')
+mock_target['entity'] = mock_target['entity'].replace('http://', '')
+
+def generate_pnp_interface_create_payload(content_from_file=False):
+ change_dir()
+ if content_from_file:
+ return (None, _pnp_create_interface_payload_file)
+
+ return (str(read_file_content(_pnp_create_interface_payload_file)), _pnp_create_interface_payload_file)
+
+
+class TestModelRepoInterfaceCreate(object):
+ @pytest.fixture(params=[201, 204, 412])
+ def serviceclient(self, mocker, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("payload_scenario", [(generate_pnp_interface_create_payload()),
+ (generate_pnp_interface_create_payload(content_from_file=True))])
+ def test_interface_create(self, fixture_cmd, serviceclient, payload_scenario):
+ payload = None
+
+ # If file path provided
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_interface_payload_file))
+
+ subject.iot_pnp_interface_create(fixture_cmd, interface_definition=payload,
+ login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ data = args[0][0].data
+ headers = args[0][0].headers
+
+ assert method == 'PUT'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert json.dumps(data)
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("payload_scenario", [(generate_pnp_interface_create_payload())])
+ def test_interface_create_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_create(fixture_cmd,
+ login=mock_target['cs'],
+ interface_definition=payload_scenario[0])
+
+ @pytest.mark.parametrize("payload_scenario, exp", [
+ (generate_pnp_interface_create_payload(), CLIError),
+ ])
+ def test_interface_create_invalid_args(self, serviceclient, payload_scenario, exp):
+ with pytest.raises(exp):
+ subject.iot_pnp_interface_create(fixture_cmd,
+ interface_definition=payload_scenario)
+
+ def test_interface_create_invalid_payload(self, serviceclient):
+
+ payload = str(read_file_content(_pnp_create_interface_payload_file))
+ payload = json.loads(payload)
+ del payload['@id']
+ payload = json.dumps(payload)
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_create(fixture_cmd, login=mock_target['cs'],
+ interface_definition=payload)
+
+
+class TestModelRepoInterfaceUpdate(object):
+
+ @pytest.fixture(params=[(200, 201), (200, 204), (200, 412)])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ payload = {
+ "continuationToken": "null",
+ "results": [
+ {
+ "comment": "",
+ "createdOn": "2019-07-09T07:46:06.044161+00:00",
+ "description": "",
+ "displayName": "MXChip 1",
+ "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"",
+ "modelName": "example:interfaces:MXChip",
+ "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb",
+ "publisherName": "microsoft.com",
+ "type": "Interface",
+ "updatedOn": "2019-07-09T21:06:00.072063+00:00",
+ "urnId": "urn:example:interfaces:MXChip:1",
+ "version": 1
+ }
+ ]
+ }
+ test_side_effect = [
+ build_mock_response(mocker, request.param[0], payload=payload),
+ build_mock_response(mocker, request.param[1], {})
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("payload_scenario",
+ [generate_pnp_interface_create_payload(),
+ generate_pnp_interface_create_payload(content_from_file=True)])
+ def test_interface_update(self, fixture_cmd, serviceclient, payload_scenario):
+ payload = None
+
+ # If file path provided
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_interface_payload_file))
+
+ subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ data = args[0][0].data
+ headers = args[0][0].headers
+
+ assert method == 'PUT'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert json.dumps(data)
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("payload_scenario",
+ [(generate_pnp_interface_create_payload()),
+ (generate_pnp_interface_create_payload(content_from_file=True))])
+ def test_model_update_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario):
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_interface_payload_file))
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+
+ @pytest.mark.parametrize("payload_scenario, exp", [
+ (generate_pnp_interface_create_payload(), CLIError),
+ ])
+ def test_interface_update_invalid_args(self, serviceclient, payload_scenario, exp):
+ with pytest.raises(exp):
+ subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload_scenario)
+
+ def test_interface_update_invalid_payload(self, serviceclient):
+
+ payload = str(read_file_content(_pnp_create_interface_payload_file))
+ payload = json.loads(payload)
+ payload['@id'] = 'fake_invalid_id'
+ payload = json.dumps(payload)
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_update(fixture_cmd, interface_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+
+
+class TestModelRepoInterfacePublish(object):
+
+ @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ payload_list = {
+ "continuationToken": "null",
+ "results": [
+ {
+ "comment": "",
+ "createdOn": "2019-07-09T07:46:06.044161+00:00",
+ "description": "",
+ "displayName": "MXChip 1",
+ "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"",
+ "modelName": "example:interfaces:MXChip",
+ "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb",
+ "publisherName": "microsoft.com",
+ "type": "Interface",
+ "updatedOn": "2019-07-09T21:06:00.072063+00:00",
+ "urnId": "urn:example:interfaces:MXChip:1",
+ "version": 1
+ }
+ ]
+ }
+ payload_show = {
+ "@id": "urn:example:interfaces:MXChip:1",
+ "@type": "Interface",
+ "displayName": "MXChip 1",
+ "contents": [
+ {
+ "@type": "Property",
+ "displayName": "Die Number",
+ "name": "dieNumber",
+ "schema": "double"
+ }
+ ],
+ "@context": "http://azureiot.com/v1/contexts/Interface.json"
+ }
+ test_side_effect = [
+ build_mock_response(mocker, request.param[0], payload=payload_list),
+ build_mock_response(mocker, request.param[1], payload=payload_show),
+ build_mock_response(mocker, request.param[2], {})
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)])
+ def test_interface_publish(self, fixture_cmd, serviceclient, target_interface):
+ subject.iot_pnp_interface_publish(fixture_cmd, interface=target_interface,
+ login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ data = args[0][0].data
+ headers = args[0][0].headers
+
+ assert method == 'PUT'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url
+ assert json.loads(data)["@id"] == _pnp_generic_interface_id
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("target_interface", [('acv.17')])
+ def test_interface_publish_error(self, fixture_cmd, serviceclient_generic_error, target_interface):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_publish(fixture_cmd, interface=target_interface,
+ login=mock_target['cs'])
+
+
+class TestModelRepoInterfaceShow(object):
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ payload = {
+ "@id": "urn:example:interfaces:MXChip:1",
+ "@type": "Interface",
+ "displayName": "MXChip 1",
+ "contents": [
+ {
+ "@type": "Property",
+ "displayName": "Die Number",
+ "name": "dieNumber",
+ "schema": "double"
+ }
+ ],
+ "@context": "http://azureiot.com/v1/contexts/Interface.json"
+ }
+ service_client.return_value = build_mock_response(mocker, request.param, payload)
+ return service_client
+
+ @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)])
+ def test_interface_show(self, fixture_cmd, serviceclient, target_interface):
+ result = subject.iot_pnp_interface_show(fixture_cmd,
+ interface=target_interface,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ headers = args[0][0].headers
+
+ assert method == 'GET'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert json.dumps(result)
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("target_interface", [('int:123')])
+ def test_interface_show_error(self, fixture_cmd, serviceclient_generic_error, target_interface):
+ with pytest.raises(CLIError):
+ result = subject.iot_pnp_interface_show(fixture_cmd,
+ interface=target_interface,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+ @pytest.fixture(params=[200])
+ def serviceclientemptyresult(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)])
+ def test_interface_show_error(self, fixture_cmd, serviceclientemptyresult, target_interface):
+ with pytest.raises(CLIError):
+ result = subject.iot_pnp_interface_show(fixture_cmd,
+ interface=target_interface,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+
+class TestModelRepoInterfaceList(object):
+ @pytest.fixture(params=[200])
+ def service_client(self, mocker, fixture_ghcs, request):
+ serviceclient = mocker.patch(path_service_client)
+ payload = {
+ "continuationToken": "null",
+ "results": [
+ {
+ "comment": "",
+ "createdOn": "2019-07-09T07:46:06.044161+00:00",
+ "description": "",
+ "displayName": "MXChip 1",
+ "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"",
+ "modelName": "example:interfaces:MXChip",
+ "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb",
+ "publisherName": "microsoft.com",
+ "type": "Interface",
+ "updatedOn": "2019-07-09T21:06:00.072063+00:00",
+ "urnId": "urn:example:interfaces:MXChip:1",
+ "version": 1
+ }
+ ]
+ }
+ serviceclient.return_value = build_mock_response(mocker, request.param, payload)
+ return serviceclient
+
+ def test_interface_list(self, fixture_cmd, service_client):
+ result = subject.iot_pnp_interface_list(fixture_cmd,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+ args = service_client.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ headers = args[0][0].headers
+
+ assert method == 'POST'
+ assert '{}/models/search?'.format(_repo_endpoint) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert len(result) == 1
+ assert headers.get('Authorization')
+
+ def test_interface_list_error(self, fixture_cmd, serviceclient_generic_error):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_list(fixture_cmd,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+
+class TestModelRepoInterfaceDelete(object):
+ @pytest.fixture(params=[204])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("target_interface", [(_pnp_generic_interface_id)])
+ def test_interface_delete(self, fixture_cmd, serviceclient, target_interface):
+ subject.iot_pnp_interface_delete(fixture_cmd,
+ interface=target_interface,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ headers = args[0][0].headers
+
+ assert method == 'DELETE'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_interface_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("target_interface", [('acv.17')])
+ def test_model_delete_error(self, fixture_cmd, serviceclient_generic_error, target_interface):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_interface_delete(fixture_cmd,
+ interface=target_interface,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+
+def generate_pnp_model_create_payload(content_from_file=False):
+ change_dir()
+ if content_from_file:
+ return (None, _pnp_create_model_payload_file)
+
+ return (str(read_file_content(_pnp_create_model_payload_file)), _pnp_create_model_payload_file)
+
+
+class TestModelRepoModelCreate(object):
+
+ @pytest.fixture(params=[201, 204, 412])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("payload_scenario",
+ [(generate_pnp_model_create_payload()),
+ (generate_pnp_model_create_payload(content_from_file=True))])
+ def test_model_create(self, fixture_cmd, serviceclient, payload_scenario):
+
+ payload = None
+
+ # If file path provided
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_model_payload_file))
+
+ subject.iot_pnp_model_create(fixture_cmd, model_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ data = args[0][0].data
+ headers = args[0][0].headers
+
+ assert method == 'PUT'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert json.dumps(data)
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("payload_scenario",
+ [(generate_pnp_model_create_payload()),
+ (generate_pnp_model_create_payload(content_from_file=True))])
+ def test_model_create_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario):
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_model_payload_file))
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_create(fixture_cmd, model_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+
+ @pytest.mark.parametrize("payload_scenario, exp", [
+ (generate_pnp_model_create_payload(), CLIError),
+ ])
+ def test_model_create_invalid_args(self, serviceclient, payload_scenario, exp):
+ with pytest.raises(exp):
+ subject.iot_pnp_model_create(fixture_cmd, model_definition=payload_scenario)
+
+ def test_model_create_invalid_payload(self, serviceclient):
+
+ payload = str(read_file_content(_pnp_create_model_payload_file))
+ payload = json.loads(payload)
+ del payload['@id']
+ payload = json.dumps(payload)
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_create(fixture_cmd, login=mock_target['cs'],
+ model_definition=payload)
+
+
+class TestModelRepoModelUpdate(object):
+
+ @pytest.fixture(params=[(200, 201), (200, 204), (200, 412)])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ payload = {
+ "continuationToken": "null",
+ "results": [
+ {
+ "comment": "",
+ "createdOn": "2019-07-09T07:46:06.044161+00:00",
+ "description": "",
+ "displayName": "Mxchip 1",
+ "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"",
+ "modelName": "example:capabilityModels:Mxchip",
+ "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb",
+ "publisherName": "microsoft.com",
+ "type": "CapabilityModel",
+ "updatedOn": "2019-07-09T21:06:00.072063+00:00",
+ "urnId": "urn:example:capabilityModels:Mxchip:1",
+ "version": 1
+ }
+ ]
+ }
+ test_side_effect = [
+ build_mock_response(mocker, request.param[0], payload=payload),
+ build_mock_response(mocker, request.param[1], {})
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("payload_scenario",
+ [generate_pnp_model_create_payload(),
+ generate_pnp_model_create_payload(content_from_file=True)])
+ def test_model_update(self, fixture_cmd, serviceclient, payload_scenario):
+
+ payload = None
+
+ # If file path provided
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_model_payload_file))
+
+ subject.iot_pnp_model_update(fixture_cmd, model_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ data = args[0][0].data
+ headers = args[0][0].headers
+
+ assert method == 'PUT'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert json.dumps(data)
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("payload_scenario",
+ [(generate_pnp_model_create_payload()),
+ (generate_pnp_model_create_payload(content_from_file=True))])
+ def test_model_update_error(self, fixture_cmd, serviceclient_generic_error, payload_scenario):
+ if not payload_scenario[0]:
+ payload = payload_scenario[1]
+ else:
+ payload = str(read_file_content(_pnp_create_model_payload_file))
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_update(fixture_cmd, model_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+
+ @pytest.mark.parametrize("payload_scenario, exp", [
+ (generate_pnp_model_create_payload(), CLIError),
+ ])
+ def test_model_update_invalid_args(self, serviceclient, payload_scenario, exp):
+ with pytest.raises(exp):
+ subject.iot_pnp_model_update(fixture_cmd, model_definition=payload_scenario)
+
+ def test_model_update_invalid_payload(self, serviceclient):
+
+ payload = str(read_file_content(_pnp_create_model_payload_file))
+ payload = json.loads(payload)
+ payload['@id'] = 'fake_invalid_id'
+ payload = json.dumps(payload)
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_update(fixture_cmd, model_definition=payload,
+ repo_endpoint=mock_target['entity'], repo_id=mock_target['repository_id'])
+
+
+class TestModelRepoModelPublish(object):
+
+ @pytest.fixture(params=[(200, 200, 201), (200, 200, 204), (200, 200, 412)])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ payload_list = {
+ "continuationToken": "null",
+ "results": [
+ {
+ "comment": "",
+ "createdOn": "2019-07-09T07:46:06.044161+00:00",
+ "description": "",
+ "displayName": "Mxchip 1",
+ "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"",
+ "modelName": "example:capabilityModels:Mxchip",
+ "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb",
+ "publisherName": "microsoft.com",
+ "type": "CapabilityModel",
+ "updatedOn": "2019-07-09T21:06:00.072063+00:00",
+ "urnId": "urn:example:capabilityModels:Mxchip:1",
+ "version": 1
+ }
+ ]
+ }
+ payload_show = {
+ "@id": "urn:example:capabilityModels:Mxchip:1",
+ "@type": "CapabilityModel",
+ "displayName": "Mxchip 1",
+ "implements": [
+ {
+ "schema":"urn:example:interfaces:MXChip:1",
+ "name":"MXChip1"
+ }
+ ],
+ "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json"
+ }
+ test_side_effect = [
+ build_mock_response(mocker, request.param[0], payload=payload_list),
+ build_mock_response(mocker, request.param[1], payload=payload_show),
+ build_mock_response(mocker, request.param[2], {})
+ ]
+ service_client.side_effect = test_side_effect
+ return service_client
+
+ @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)])
+ def test_model_publish(self, fixture_cmd, serviceclient, target_model):
+ subject.iot_pnp_model_publish(fixture_cmd, model=target_model,
+ login=mock_target['cs'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ data = args[0][0].data
+ headers = args[0][0].headers
+
+ assert method == 'PUT'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url
+ assert json.loads(data)["@id"] == _pnp_generic_model_id
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("target_model", [('acv.17')])
+ def test_model_publish_error(self, fixture_cmd, serviceclient_generic_error, target_model):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_publish(fixture_cmd, model=target_model,
+ login=mock_target['cs'])
+
+
+class TestModelRepoModelShow(object):
+ @pytest.fixture(params=[200])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ payload = {
+ "@id": "urn:example:capabilityModels:Mxchip:1",
+ "@type": "CapabilityModel",
+ "displayName": "Mxchip 1",
+ "implements": [
+ {
+ "schema":"urn:example:interfaces:MXChip:1",
+ "name":"MXChip1"
+ }
+ ],
+ "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json"
+ }
+ service_client.return_value = build_mock_response(mocker, request.param, payload=payload)
+ return service_client
+ @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)])
+ def test_model_show(self, fixture_cmd, serviceclient, target_model):
+ result = subject.iot_pnp_model_show(fixture_cmd,
+ model=target_model,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ headers = args[0][0].headers
+
+ assert method == 'GET'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert json.dumps(result)
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("target_model", [('acv:17')])
+ def test_model_show_error(self, fixture_cmd, serviceclient_generic_error, target_model):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_show(fixture_cmd,
+ model=target_model,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+ @pytest.fixture(params=[200])
+ def serviceclientemptyresult(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)])
+ def test_model_show_no_result(self, fixture_cmd, serviceclientemptyresult, target_model):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_show(fixture_cmd,
+ model=target_model,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+
+class TestModelRepoModelList(object):
+ @pytest.fixture(params=[200])
+ def service_client(self, mocker, fixture_ghcs, request):
+ serviceclient = mocker.patch(path_service_client)
+ payload = {
+ "continuationToken": "null",
+ "results": [
+ {
+ "comment": "",
+ "createdOn": "2019-07-09T07:46:06.044161+00:00",
+ "description": "",
+ "displayName": "Mxchip 1",
+ "etag": "\"41006e67-0000-0800-0000-5d2501b80000\"",
+ "modelName": "example:capabilityModels:Mxchip",
+ "publisherId": "aabbaabb-aabb-aabb-aabb-aabbaabbaabb",
+ "publisherName": "microsoft.com",
+ "type": "CapabilityModel",
+ "updatedOn": "2019-07-09T21:06:00.072063+00:00",
+ "urnId": "urn:example:capabilityModels:Mxchip:1",
+ "version": 1
+ }
+ ]
+ }
+ serviceclient.return_value = build_mock_response(mocker, request.param, payload)
+ return serviceclient
+
+ def test_model_list(self, fixture_cmd, service_client):
+ result = subject.iot_pnp_model_list(fixture_cmd,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+ args = service_client.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ headers = args[0][0].headers
+
+ assert method == 'POST'
+ assert '{}/models/search?'.format(_repo_endpoint, _repo_id) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert len(result) == 1
+ assert headers.get('Authorization')
+
+ def test_model_list_error(self, fixture_cmd, serviceclient_generic_error):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_list(fixture_cmd,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+
+
+class TestModelRepoModelDelete(object):
+
+ @pytest.fixture(params=[204])
+ def serviceclient(self, mocker, fixture_ghcs, request):
+ service_client = mocker.patch(path_service_client)
+ service_client.return_value = build_mock_response(mocker, request.param, {})
+ return service_client
+
+ @pytest.mark.parametrize("target_model", [(_pnp_generic_model_id)])
+ def test_model_delete(self, fixture_cmd, serviceclient, target_model):
+ subject.iot_pnp_model_delete(fixture_cmd,
+ model=target_model,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
+ args = serviceclient.call_args
+ url = args[0][0].url
+ method = args[0][0].method
+ headers = args[0][0].headers
+
+ assert method == 'DELETE'
+ assert '{}/models/{}?'.format(_repo_endpoint, url_encode_str(_pnp_generic_model_id, plus=True)) in url
+ assert 'repositoryId={}'.format(_repo_id) in url
+ assert headers.get('Authorization')
+
+ @pytest.mark.parametrize("target_model", [('acv.17')])
+ def test_model_delete_error(self, fixture_cmd, serviceclient_generic_error, target_model):
+ with pytest.raises(CLIError):
+ subject.iot_pnp_model_delete(fixture_cmd,
+ model=target_model,
+ repo_endpoint=mock_target['entity'],
+ repo_id=mock_target['repository_id'])
diff --git a/azext_iot/tests/test_pnp_create_payload_interface.json b/azext_iot/tests/test_pnp_create_payload_interface.json
new file mode 100644
index 000000000..422f2deb8
--- /dev/null
+++ b/azext_iot/tests/test_pnp_create_payload_interface.json
@@ -0,0 +1,210 @@
+{
+ "@id": "urn:example:interfaces:MXChip:1",
+ "@type": "Interface",
+ "displayName": "MXChip1",
+ "contents": [
+ {
+ "@type": "Property",
+ "name": "dieNumber",
+ "displayName": "Die Number",
+ "schema": "double"
+ },
+ {
+ "@type": "Property",
+ "name": "setCurrent",
+ "displayName": "Current",
+ "writable": true,
+ "schema": "double",
+ "displayUnit": "amps"
+ },
+ {
+ "@type": "Property",
+ "name": "setVoltage",
+ "displayName": "Voltage",
+ "writable": true,
+ "schema": "double",
+ "displayUnit": "volts"
+ },
+ {
+ "@type": "Property",
+ "name": "fanSpeed",
+ "displayName": "Fan Speed",
+ "writable": true,
+ "schema": "double",
+ "displayUnit": "rpm"
+ },
+ {
+ "@type": "Property",
+ "name": "activateIR",
+ "displayName": "IR",
+ "writable": true,
+ "schema": "boolean"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "humidity",
+ "displayName": "Humidity",
+ "schema": "double",
+ "displayUnit": "%"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "pressure",
+ "displayName": "Pressure",
+ "schema": "double",
+ "displayUnit": "hPa"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "magnetometer",
+ "displayName": "Magnetometer",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "x",
+ "schema": "double",
+ "displayUnit": "mgauss"
+ },
+ {
+ "name": "y",
+ "schema": "double",
+ "displayUnit": "mgauss"
+ },
+ {
+ "name": "z",
+ "schema": "double",
+ "displayUnit": "mgauss"
+ }
+ ]
+ }
+ },
+ {
+ "@type": "Telemetry",
+ "name": "accelerometer",
+ "displayName": "Accelerometer",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "x",
+ "schema": "double",
+ "displayUnit": "mg"
+ },
+ {
+ "name": "y",
+ "schema": "double",
+ "displayUnit": "mg"
+ },
+ {
+ "name": "z",
+ "schema": "double",
+ "displayUnit": "mg"
+ }
+ ]
+ }
+ },
+ {
+ "@type": "Telemetry",
+ "name": "gyroscope",
+ "displayName": "Gyroscope",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "x",
+ "schema": "double",
+ "displayUnit": "mdps"
+ },
+ {
+ "name": "y",
+ "schema": "double",
+ "displayUnit": "mdps"
+ },
+ {
+ "name": "z",
+ "schema": "double",
+ "displayUnit": "mdps"
+ }
+ ]
+ }
+ },
+ {
+ "@type": "Telemetry",
+ "name": "buttonBPressed",
+ "displayName": "Button B Pressed",
+ "schema": "string"
+ },
+ {
+ "@type": "Telemetry",
+ "name": "deviceState",
+ "displayName": "Device State",
+ "schema": {
+ "@type": "Enum",
+ "valueSchema":"string",
+ "enumValues": [
+ {
+ "name": "normal",
+ "displayName": "Normal",
+ "enumValue": "NORMAL"
+ },
+ {
+ "name": "danger",
+ "displayName": "Danger",
+ "enumValue": "DANGER"
+ },
+ {
+ "name": "caution",
+ "displayName": "Caution",
+ "enumValue": "CAUTION"
+ }
+ ]
+ }
+ },
+ {
+ "@type": "Command",
+ "name": "echo",
+ "displayName": "Echo",
+ "request": {
+ "name": "name1",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "displayedValue",
+ "displayName": "Value to display",
+ "schema": "string"
+ }
+ ]
+ }
+ },
+ "response": {
+ "name" : "name2",
+ "schema": "string"
+ }
+ },
+ {
+ "@type": "Command",
+ "name": "countdown",
+ "displayName": "Countdown",
+ "request": {
+ "name": "name1",
+ "schema": {
+ "@type": "Object",
+ "fields": [
+ {
+ "name": "countFrom",
+ "displayName": "Count from",
+ "schema": "double"
+ }
+ ]
+ }
+ },
+ "response": {
+ "name" : "name3",
+ "schema": "double"
+ }
+ }
+ ],
+ "@context": "http://azureiot.com/v1/contexts/Interface.json"
+}
\ No newline at end of file
diff --git a/azext_iot/tests/test_pnp_create_payload_model.json b/azext_iot/tests/test_pnp_create_payload_model.json
new file mode 100644
index 000000000..7546052a9
--- /dev/null
+++ b/azext_iot/tests/test_pnp_create_payload_model.json
@@ -0,0 +1,12 @@
+{
+ "@id": "urn:example:capabilityModels:Mxchip:1",
+ "@type": "CapabilityModel",
+ "displayName": "Mxchip 1",
+ "implements": [
+ {
+ "schema":"urn:example:interfaces:MXChip:1",
+ "name":"MXChip1"
+ }
+ ],
+ "@context": "http://azureiot.com/v1/contexts/CapabilityModel.json"
+}
\ No newline at end of file
diff --git a/azext_iot/tests/test_pnp_interface_list.json b/azext_iot/tests/test_pnp_interface_list.json
new file mode 100644
index 000000000..efad99c7b
--- /dev/null
+++ b/azext_iot/tests/test_pnp_interface_list.json
@@ -0,0 +1,19 @@
+{
+ "continuationToken": null,
+ "results": [
+ {
+ "comment": "Requires temperature and humidity sensors.",
+ "createdOn": "2019-07-03T22:42:21.929126+00:00",
+ "description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties",
+ "displayName": "Environmental Sensor",
+ "etag": "\"110043fc-0000-0800-0000-5d27a3960000\"",
+ "modelName": "contoso:com:EnvironmentalSensor",
+ "publisherId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
+ "publisherName": "microsoft.com",
+ "type": "Interface",
+ "updatedOn": "2019-07-03T22:42:21.929126+00:00",
+ "urnId": "urn:contoso:com:EnvironmentalSensor:1",
+ "version": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/azext_iot/tests/test_pnp_interface_show.json b/azext_iot/tests/test_pnp_interface_show.json
new file mode 100644
index 000000000..14dae7fbd
--- /dev/null
+++ b/azext_iot/tests/test_pnp_interface_show.json
@@ -0,0 +1,89 @@
+{
+ "@context": "http://azureiot.com/v1/contexts/Interface.json",
+ "@id": "urn:contoso:com:EnvironmentalSensor:1",
+ "@type": "Interface",
+ "comment": "Requires temperature and humidity sensors.",
+ "contents": [
+ {
+ "@type": "Property",
+ "description": "The state of the device. Two states online/offline are available.",
+ "displayName": "Device State",
+ "name": "state",
+ "schema": "boolean"
+ },
+ {
+ "@type": "Property",
+ "description": "The name of the customer currently operating the device.",
+ "displayName": "Customer Name",
+ "name": "name",
+ "schema": "string",
+ "writable": true
+ },
+ {
+ "@type": "Property",
+ "description": "The brightness level for the light on the device. Can be specified as 1 (high), 2 (medium), 3 (low)",
+ "displayName": "Brightness Level",
+ "name": "brightness",
+ "schema": "long",
+ "writable": true
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "SemanticType/Temperature"
+ ],
+ "description": "Current temperature on the device",
+ "displayName": "Temperature",
+ "name": "temp",
+ "schema": "double",
+ "unit": "Units/Temperature/fahrenheit"
+ },
+ {
+ "@type": [
+ "Telemetry",
+ "SemanticType/Humidity"
+ ],
+ "description": "Current humidity on the device",
+ "displayName": "Humidity",
+ "name": "humid",
+ "schema": "double",
+ "unit": "Units/Humidity/percent"
+ },
+ {
+ "@type": "Command",
+ "commandType": "synchronous",
+ "description": "This command will begin blinking the LED for given time interval.",
+ "name": "blink",
+ "request": {
+ "name": "blinkRequest",
+ "schema": "long"
+ },
+ "response": {
+ "name": "blinkResponse",
+ "schema": "string"
+ }
+ },
+ {
+ "@type": "Command",
+ "commandType": "synchronous",
+ "comment": "This Commands will turn-on the LED light on the device.",
+ "name": "turnon",
+ "response": {
+ "name": "turnonResponse",
+ "schema": "string"
+ }
+ },
+ {
+ "@type": "Command",
+ "commandType": "synchronous",
+ "comment": "This Commands will turn-off the LED light on the device.",
+ "name": "turnoff",
+ "response": {
+ "name": "turnoffResponse",
+ "schema": "string"
+ }
+ }
+ ],
+ "description": "Provides functionality to report temperature, humidity. Provides telemetry, commands and read-write properties",
+ "displayName": "Environmental Sensor"
+ }
\ No newline at end of file
diff --git a/scripts/ci/test_source.sh b/scripts/ci/test_source.sh
index 8595114b8..b088b8019 100644
--- a/scripts/ci/test_source.sh
+++ b/scripts/ci/test_source.sh
@@ -31,4 +31,10 @@ pytest -v azext_iot/tests/test_iot_dps_unit.py
echo "Executing - Utility unit tests"
pytest -v azext_iot/tests/test_iot_utility_unit.py
+echo "Executing - Pnp unit tests"
+pytest -v azext_iot/tests/test_iot_pnp_unit.py
+
+echo "Executing - Digitaltwin unit tests"
+pytest -v azext_iot/tests/test_iot_digitaltwin_unit.py
+
echo "Tests completed."
diff --git a/scripts/ci/test_static.sh b/scripts/ci/test_static.sh
index 4aa56fcf0..47f300158 100644
--- a/scripts/ci/test_static.sh
+++ b/scripts/ci/test_static.sh
@@ -4,5 +4,5 @@ set -e
proc_number=`python -c 'import multiprocessing; print(multiprocessing.cpu_count())'`
# Run pylint/flake8 on IoT extension
-pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk --rcfile=./.pylintrc -j $proc_number
+pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk,pnp_sdk --rcfile=./.pylintrc -j $proc_number
flake8 --statistics --exclude=*_sdk --append-config=./.flake8 azext_iot/
diff --git a/scripts/ci/test_static_py2.sh b/scripts/ci/test_static_py2.sh
index fce599a33..4e8ab57ab 100644
--- a/scripts/ci/test_static_py2.sh
+++ b/scripts/ci/test_static_py2.sh
@@ -4,5 +4,5 @@ set -e
proc_number=`python -c 'import multiprocessing; print(multiprocessing.cpu_count())'`
# Run pylint/flake8 on IoT extension
-pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk,events3 --rcfile=./.pylintrc -j $proc_number
+pylint azext_iot/ --ignore=models,service_sdk,device_sdk,custom_sdk,dps_sdk,pnp_sdk,events3 --rcfile=./.pylintrc -j $proc_number
flake8 --statistics --exclude=*_sdk,events3 --append-config=./.flake8 azext_iot/