IoTTestOrchestrator + Driver Extensibility Blog

Good afternoon,

I’m using IoTTestOrchestrator along with a custom template to define the driver configuration.

During test execution, I couldn’t find the driver configuration anywhere. Is the configuration defined in the custom template supposed to be visible during the tests, or is it expected that it does not appear at all?

If it is expected to be available, does anyone have an example they could share showing how to access or use the driver configuration from a custom template?

Thanks in advance!

Hi, I don’t understand your question. Can you give more detail on what are you asking? I can provide you with a test example of using a custom templates task, but I am not sure that is what you want.

[TestMethod]
        public void DriverCommandGetCurrentDefinitionsCustomTemplate()
        {
            IoTTestOrchestrator.MockSystem.PluginMain system = (testContext.System as IoTTestOrchestrator.MockSystem.PluginMain);

            system.WaitForAllComunicating(40);
            var controllerInstance = testContext.Instances["Controller"] as IControllerInstanceContext;
            var hasBeenCalled = false;
            Exception? callbackException = null;

            controllerInstance.SystemEvent("Cmf.Test.DF.DriverCommand.GetCurrentDefinition.CustomTemplate.Request", new
            {
                message = "Activation"
            },
            SendType.SendRequest,
            (evt) =>
            {
                try
                {
                    JObject payload = JObject.Parse(evt);
                    JObject json = payload["reply"] as JObject ?? payload;

                    var properties = (JArray)json["properties"]!;
                    var events = (JArray)json["events"]!;
                    var commands = (JArray)json["commands"]!;

                    var customProperty = properties.Children<JObject>().SingleOrDefault(p => p["name"]?.ToString() == "CurrentDefinitionsCustomProperty");
                    Assert.IsNotNull(customProperty, "Expected custom property to be included in the current definitions reply.");
                    Assert.AreEqual("AccessTime", customProperty!["deviceId"]?.ToString());
                    Assert.AreEqual("AccessTime", customProperty["extendedData"]?["propertyType"]?.ToString());

                    var customEvent = events.Children<JObject>().SingleOrDefault(e => e["name"]?.ToString() == "CurrentDefinitionsCustomEvent");
                    Assert.IsNotNull(customEvent, "Expected custom event to be included in the current definitions reply.");
                    Assert.AreEqual("DeletedFile", customEvent!["deviceId"]?.ToString());
                    Assert.AreEqual("DeletedFile", customEvent["extendedData"]?["eventTrigger"]?.ToString());
                    Assert.AreEqual("CurrentDefinitionsCustomProperty", customEvent["properties"]?.First?["name"]?.ToString());
                    Assert.AreEqual("None", customEvent["properties"]?.First?["extendedData"]?["identifierType"]?.ToString());

                    var customCommand = commands.Children<JObject>().SingleOrDefault(c => c["name"]?.ToString() == "CurrentDefinitionsCustomCommand");
                    Assert.IsNotNull(customCommand, "Expected custom command to be included in the current definitions reply.");
                    Assert.AreEqual("Exists", customCommand!["deviceId"]?.ToString());
                    Assert.AreEqual("Exists", customCommand["extendedData"]?["commandType"]?.ToString());
                    Assert.AreEqual("CurrentDefinitionsCustomParameter", customCommand["parameters"]?.First?["name"]?.ToString());
                    Assert.AreEqual("CurrentDefinitionsCustomDefaultValue", customCommand["parameters"]?.First?["defaultValue"]?.ToString());
                    Assert.AreEqual("CurrentDefinitionsCustomParameterSource", customCommand["parameters"]?.First?["extendedData"]?["parameterSource"]?.ToString());

                    hasBeenCalled = true;
                }
                catch (Exception ex)
                {
                    callbackException = ex;
                }
            });

            testScenario.Utilities.WaitFor(20, "It was never invoked", () =>
            {
                return hasBeenCalled || callbackException != null;
            });

            if (callbackException != null)
            {
                throw new AssertFailedException($"Reply validation failed: {callbackException.Message}", callbackException);
            }
        }
{
  "tasks": [
    {
      "id": "task_101",
      "reference": {
        "name": "customTemplates",
        "package": {
          "name": "@criticalmanufacturing/connect-iot-controller-engine-core-tasks",
          "version": "12.0.0-dev"
        }
      },
      "settings": {
        "values": [
          {
            "name": "CurrentDefinitionsCustomTemplate",
            "description": "Adds custom definitions to validate CurrentDefinitions reply",
            "json": {
              "properties": [
                {
                  "Name": "CurrentDefinitionsCustomProperty",
                  "DevicePropertyId": "CurrentDefinitionsCustomProperty",
                  "DataType": "String",
                  "AutomationProtocolDataType": "String",
                  "IsReadable": true,
                  "IsWritable": false,
                  "ExtendedData": {
                    "propertyType": "AccessTime"
                  }
                }
              ],
              "events": [
                {
                  "Name": "CurrentDefinitionsCustomEvent",
                  "DeviceEventId": "CurrentDefinitionsCustomEvent",
                  "IsEnabled": true,
                  "ExtendedData": {
                    "eventTrigger": "DeletedFile"
                  },
                  "EventProperties": [
                    {
                      "Property": "CurrentDefinitionsCustomProperty",
                      "Order": 1,
                      "ExtendedData": {
                        "identifierType": "None"
                      }
                    }
                  ]
                }
              ],
              "commands": [
                {
                  "Name": "CurrentDefinitionsCustomCommand",
                  "DeviceCommandId": "CurrentDefinitionsCustomCommand",
                  "ExtendedData": {
                    "commandType": "Exists"
                  },
                  "CommandParameters": [
                    {
                      "Name": "CurrentDefinitionsCustomParameter",
                      "Order": 1,
                      "DataType": "String",
                      "AutomationProtocolDataType": "String",
                      "DefaultValue": "CurrentDefinitionsCustomDefaultValue",
                      "IsMandatory": false,
                      "ExtendedData": {
                        "parameterSource": "CurrentDefinitionsCustomParameterSource"
                      }
                    }
                  ]
                }
              ]
            }
          }
        ],
        "___cmf___name": "Custom Driver Definition"
      },
      "driver": "Handler"
    },
    {
      "id": "task_102",
      "reference": {
        "name": "driverCommand",
        "package": {
          "name": "@criticalmanufacturing/connect-iot-controller-engine-core-tasks",
          "version": "12.0.0-dev"
        }
      },
      "settings": {
        "command": "CurrentDefinitions",
        "___cmf___name": "Driver Command"
      },
      "driver": "Handler"
    },
    {
      "id": "task_103",
      "reference": {
        "name": "systemActionReply",
        "package": {
          "name": "@criticalmanufacturing/connect-iot-controller-engine-core-tasks",
          "version": "12.0.0-dev"
        }
      },
      "settings": {
        "defaultReply": {},
        "___cmf___name": "System Action Reply"
      }
    },
    {
      "id": "task_104",
      "reference": {
        "name": "systemActionListener",
        "package": {
          "name": "@criticalmanufacturing/connect-iot-controller-engine-core-tasks",
          "version": "12.0.0-dev"
        }
      },
      "settings": {
        "autoEnable": true,
        "actionGroup": "Cmf.Test.DF.DriverCommand.GetCurrentDefinition.CustomTemplate.Request",
        "replyTimeout": 60000,
        "___cmf___name": "System Action Listener"
      }
    }
  ],
  "converters": [],
  "links": [
    {
      "id": "link_101",
      "sourceId": "task_104",
      "targetId": "task_101",
      "inputName": "activate",
      "outputName": "success"
    },
    {
      "id": "link_102",
      "sourceId": "task_101",
      "targetId": "task_102",
      "inputName": "activate",
      "outputName": "success"
    },
    {
      "id": "link_103",
      "sourceId": "task_102",
      "targetId": "task_103",
      "inputName": "reply",
      "outputName": "reply"
    },
    {
      "id": "link_104",
      "sourceId": "task_102",
      "targetId": "task_103",
      "inputName": "activate",
      "outputName": "success"
    }
  ],
  "layout": {
    "general": {
      "color": null,
      "notes": []
    },
    "drawers": {
      "DIAGRAM": {
        "tasks": {
          "task_101": {
            "collapsed": false,
            "position": {
              "x": 619,
              "y": 136
            },
            "outdated": false
          },
          "task_102": {
            "collapsed": false,
            "position": {
              "x": 1157,
              "y": 136
            },
            "outdated": false
          },
          "task_103": {
            "collapsed": false,
            "position": {
              "x": 1703,
              "y": 194
            },
            "outdated": false
          },
          "task_104": {
            "collapsed": false,
            "position": {
              "x": 104,
              "y": 111
            },
            "outdated": false
          }
        },
        "links": {
          "link_101": {
            "vertices": []
          },
          "link_102": {
            "vertices": []
          },
          "link_103": {
            "vertices": []
          },
          "link_104": {
            "vertices": []
          }
        },
        "notes": {},
        "pan": {
          "x": 0,
          "y": 0
        },
        "zoom": 0.58
      }
    }
  }
}

Hi Domingos,

Short answer: what you are seeing is expected, the driver configuration comes from a custom template, it is not imported by the orchestrator, and it fails silently.

Why it doesn’t appear

The MockSystem plugin only imports a fixed set of sections from a masterdata/template file:

<DM>AutomationController, <DM>AutomationDriverDefinition, <DM>AutomationProtocol, <DM>AutomationManager, <DM>AutomationWorkflow, <LOOKUP>AutomationInstances, AutomationControllerDriverDef, AutomationControllerWorkflow, AutomationProperty, AutomationEvent, AutomationEventProperties, AutomationCommand, AutomationCommandParameter.

Anything else in the file is skipped without a warning. In particular, <SM>LookupTableValues (where CustomAutomationConfiguration entries live) and any MES config sections are never read. IsTemplate is ignored too.

How to make the configuration available

You have to feed it to the mock explicitly. Two channels, depending on how your driver reads it.

1. Driver reads MES configs (the usual case)

Use .AddConfig(path, value); those are served on /api/Configuration/GetConfigsForPaths:

.SystemPlugin<MockSystem.PluginMain>(new MockSystem.Plugin.SettingsBuilder()
    .MasterdataPath(@"Cmf.Custom.IoT.Data/MasterData/MyCustomController.json")
    .AddSite("Site00001")
    .AddManager("Manager01")
    .AddControllerInstance("My Custom Controller", "Manager01", "Site00001")
    .AddDriverInstance("My Custom Controller", "Driver1", "Manager01", "Site00001")
    // the driver configuration your template would normally provide:
    .AddConfig("/Automation/MyDriver/BaseURL", "http://localhost:8080")
    .AddConfig("/Automation/MyDriver/AuthenticationMode", "None")
    .Build())

Worth knowing:

  • The lookup is an exact string match on the path
  • To find the exact paths your driver asks for, run the test once and grep the log for Config '<path>' was not defined!. That warning is emitted per missing path and is the fastest way to get the list right.

2. Driver reads entity attributes

If your configuration is resolved from entity attributes (for example ${AttributeName} tokens in device property IDs), seed them with the raw AddEntity overload — AddResource / AddEntityByName don’t take attributes:

.AddEntity("Resource", @"{
    ""Id"": 1001,
    ""Name"": ""Resource1"",
    ""Attributes"": { ""DriverPort"": ""5000"", ""DriverAddress"": ""127.0.0.1"" }
}")

Those are returned by /api/GenericService/LoadObjectAttributes, and are what ${...} / $(...) tokens resolve against.


If you can share which template you’re using (master data export vs. scenario config template) and how your driver reads the value, it will be easier to point you in the right direction.

Regards