author | ms.author | ms.service | ms.topic | ms.date |
---|---|---|---|---|
dominicbetts |
dobett |
iot-pnp |
include |
11/19/2020 |
To announce the model ID, the device must include it in the connection information:
DeviceClient.CreateFromConnectionString(
connectionString,
TransportType.Mqtt,
new ClientOptions() { ModelId = modelId })
The new ClientOptions
overload is available in all DeviceClient
methods used to initialize a connection.
Tip
For modules and IoT Edge, use ModuleClient
in place of DeviceClient
.
Devices using the Device Provisioning Service (DPS) can include the modelId
to be used during the provisioning process using the following JSON payload.
{
"modelId" : "dtmi:com:example:Thermostat;1"
}
As described in Understand components in IoT Plug and Play models, device builders must decide if they want to use components to describe their devices. When using components, devices must follow the rules described in this section.
A default component doesn't require any special property.
When using nested components, devices must set a message property with the component name:
public async Task SendComponentTelemetryValueAsync(string componentName, string serializedTelemetry)
{
var message = new Message(Encoding.UTF8.GetBytes(serializedTelemetry));
message.ComponentName = componentName;
message.ContentType = "application/json";
message.ContentEncoding = "utf-8";
await client.SendEventAsync(message);
}
Reporting a property from the default component doesn't require any special construct:
TwinCollection reportedProperties = new TwinCollection();
reportedProperties["maxTemperature"] = 38.7;
await client.UpdateReportedPropertiesAsync(reportedProperties);
The device twin is updated with the next reported property:
{
"reported": {
"maxTemperature" : 38.7
}
}
When using nested components, properties must be created within the component name:
TwinCollection reportedProperties = new TwinCollection();
TwinCollection component = new TwinCollection();
component["maxTemperature"] = 38.7;
component["__t"] = "c"; // marker to identify a component
reportedProperties["thermostat1"] = component;
await client.UpdateReportedPropertiesAsync(reportedProperties);
The device twin is updated with the next reported property:
{
"reported": {
"thermostat1" : {
"__t" : "c",
"maxTemperature" : 38.7
}
}
}
These properties can be set by the device or updated by the solution. If the solution updates a property, the client receives a notification as a callback in the DeviceClient
or ModuleClient
. To follow the IoT Plug and Play conventions, the device must inform the service that the property was successfully received.
When a device reports a writable property, it must include the ack
values defined in the conventions.
To report a writable property from the default component:
TwinCollection reportedProperties = new TwinCollection();
TwinCollection ackProps = new TwinCollection();
ackProps["value"] = 23.2;
ackProps["ac"] = 200; // using HTTP status codes
ackProps["av"] = 0; // not readed from a desired property
ackProps["ad"] = "reported default value";
reportedProperties["targetTemperature"] = ackProps;
await client.UpdateReportedPropertiesAsync(reportedProperties);
The device twin is updated with the next reported property:
{
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
To report a writable property from a nested component, the twin must include a marker:
TwinCollection reportedProperties = new TwinCollection();
TwinCollection component = new TwinCollection();
TwinCollection ackProps = new TwinCollection();
component["__t"] = "c"; // marker to identify a component
ackProps["value"] = 23.2;
ackProps["ac"] = 200; // using HTTP status codes
ackProps["av"] = 0; // not read from a desired property
ackProps["ad"] = "reported default value";
component["targetTemperature"] = ackProps;
reportedProperties["thermostat1"] = component;
await client.UpdateReportedPropertiesAsync(reportedProperties);
The device twin is updated with the next reported property:
{
"reported": {
"thermostat1": {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
Services can update desired properties that trigger a notification on the connected devices. This notification includes the updated desired properties, including the version number identifying the update. Devices must respond with the same ack
message as reported properties.
A default component sees the single property and creates the reported ack
with the received version:
await client.SetDesiredPropertyUpdateCallbackAsync(async (desired, ctx) =>
{
JValue targetTempJson = desired["targetTemperature"];
double targetTemperature = targetTempJson.Value<double>();
TwinCollection reportedProperties = new TwinCollection();
TwinCollection ackProps = new TwinCollection();
ackProps["value"] = targetTemperature;
ackProps["ac"] = 200;
ackProps["av"] = desired.Version;
ackProps["ad"] = "desired property received";
reportedProperties["targetTemperature"] = ackProps;
await client.UpdateReportedPropertiesAsync(reportedProperties);
}, null);
The device twin shows the property in the desired and reported sections:
{
"desired" : {
"targetTemperature": 23.2,
"$version" : 3
},
"reported": {
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
A nested component receives the desired properties wrapped with the component name, and should report back the ack
reported property:
await client.SetDesiredPropertyUpdateCallbackAsync(async (desired, ctx) =>
{
JObject thermostatComponent = desired["thermostat1"];
JToken targetTempProp = thermostatComponent["targetTemperature"];
double targetTemperature = targetTempProp.Value<double>();
TwinCollection reportedProperties = new TwinCollection();
TwinCollection component = new TwinCollection();
TwinCollection ackProps = new TwinCollection();
component["__t"] = "c"; // marker to identify a component
ackProps["value"] = targetTemperature;
ackProps["ac"] = 200; // using HTTP status codes
ackProps["av"] = desired.Version; // not readed from a desired property
ackProps["ad"] = "desired property received";
component["targetTemperature"] = ackProps;
reportedProperties["thermostat1"] = component;
await client.UpdateReportedPropertiesAsync(reportedProperties);
}, null);
The device twin for a nested component shows the desired and reported sections as follows:
{
"desired" : {
"thermostat1" : {
"__t" : "c",
"targetTemperature": 23.2,
}
"$version" : 3
},
"reported": {
"thermostat1" : {
"__t" : "c",
"targetTemperature": {
"value": 23.2,
"ac": 200,
"av": 3,
"ad": "complete"
}
}
}
}
A default component receives the command name as it was invoked by the service.
A nested component receives the command name prefixed with the component name and the *
separator.
await client.SetMethodHandlerAsync("themostat*reboot", (MethodRequest req, object ctx) =>
{
Console.WriteLine("REBOOT");
return Task.FromResult(new MethodResponse(200));
},
null);
Commands use types to define their request and response payloads. A device must deserialize the incoming input parameter and serialize the response. The following example shows how to implement a command with complex types defined in the payloads:
{
"@type": "Command",
"name": "start",
"request": {
"name": "startRequest",
"schema": {
"@type": "Object",
"fields": [
{
"name": "startPriority",
"schema": "integer"
},
{
"name": "startMessage",
"schema" : "string"
}
]
}
},
"response": {
"name": "startReponse",
"schema": {
"@type": "Object",
"fields": [
{
"name": "startupTime",
"schema": "integer"
},
{
"name": "startupMessage",
"schema": "string"
}
]
}
}
}
The following code snippets show how a device implements this command definition, including the types used to enable serialization and deserialization:
class startRequest
{
public int startPriority { get; set; }
public string startMessage { get; set; }
}
class startResponse
{
public int startupTime { get; set; }
public string startupMessage { get; set; }
}
// ...
await client.SetMethodHandlerAsync("start", (MethodRequest req, object ctx) =>
{
var startRequest = JsonConvert.DeserializeObject<startRequest>(req.DataAsJson);
Console.WriteLine($"Received start command with priority ${startRequest.startPriority} and ${startRequest.startMessage}");
var startResponse = new startResponse
{
startupTime = 123,
startupMessage = "device started with message " + startRequest.startMessage
};
string responsePayload = JsonConvert.SerializeObject(startResponse);
MethodResponse response = new MethodResponse(Encoding.UTF8.GetBytes(responsePayload), 200);
return Task.FromResult(response);
},null);
Tip
The request and response names aren't present in the serialized payloads transmitted over the wire.