From 3f02cfab0f8bb3292663c560f9046189d8a4379e Mon Sep 17 00:00:00 2001 From: Darren Chuang Date: Tue, 29 Nov 2016 13:23:11 +0800 Subject: [PATCH] Report supported methods --- Common/Common.csproj | 2 + Common/Factory/SampleDeviceFactory.cs | 8 +- Common/Helpers/SupportedMethodsHelper.cs | 116 ++++++++++++++++++ Common/Models/SupportedMethod.cs | 18 +++ .../BusinessLogic/DeviceLogic.cs | 2 + .../iothub_client_sample_device_method.c | 2 +- .../SimulatorCore/Devices/DeviceBase.cs | 10 +- .../Common/SupportedMethodsHelperTests.cs | 77 ++++++++++++ UnitTests/UnitTests.csproj | 1 + 9 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 Common/Helpers/SupportedMethodsHelper.cs create mode 100644 Common/Models/SupportedMethod.cs create mode 100644 UnitTests/Common/SupportedMethodsHelperTests.cs diff --git a/Common/Common.csproj b/Common/Common.csproj index 2d085b93..1dc11826 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -175,6 +175,7 @@ + @@ -188,6 +189,7 @@ + diff --git a/Common/Factory/SampleDeviceFactory.cs b/Common/Factory/SampleDeviceFactory.cs index e08aa2e4..b96d5dfc 100644 --- a/Common/Factory/SampleDeviceFactory.cs +++ b/Common/Factory/SampleDeviceFactory.cs @@ -154,10 +154,16 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Factory )); device.Commands.Add(new Command( "ChangeDeviceState", - DeliveryType.Method, + DeliveryType.Message, "Sets the device state metadata property that the device reports. This is useful for testing back-end logic.", new[] { new Parameter("DeviceState", "string") } )); + device.Commands.Add(new Command( + "ChangeDeviceState", + DeliveryType.Method, + "Sets the device state metadata property that the device reports. This is useful for testing back-end logic.", + new[] { new Parameter("DeviceState", "string") } + )); } public static List GetDefaultDeviceNames() diff --git a/Common/Helpers/SupportedMethodsHelper.cs b/Common/Helpers/SupportedMethodsHelper.cs new file mode 100644 index 00000000..7b80e480 --- /dev/null +++ b/Common/Helpers/SupportedMethodsHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Models; +using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Models.Commands; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Helpers +{ + + public static class SupportedMethodsHelper + { + private static string SupportedMethodsKey = "SupportedMethods"; + + public static JObject GenerateSupportedMethodsReportedProperty(List commands) + { + var methods = new Dictionary(); + foreach (var command in commands.Where(c => c.DeliveryType == DeliveryType.Method)) + { + string normalizedMethodName = NormalizeMethodName(command); + methods[normalizedMethodName] = Convert(command); + } + + var obj = new JObject(); + obj[SupportedMethodsKey] = JObject.FromObject(methods); + + return obj; + } + + public static void AddSupportedMethodsFromReportedProperty(DeviceModel device, Twin twin) + { + if (!twin.Properties.Reported.Contains(SupportedMethodsKey)) + { + return; + } + + // The property could be either TwinColltion or JObject that depends on where the property is generated from + // Convert the property to JObject to unify the type. + var methods = JObject.FromObject(twin.Properties.Reported[SupportedMethodsKey]).ToObject>(); + + foreach (var method in methods) + { + var command = Convert(method.Value); + + if (command != null && !device.Commands.Any(c => c.Name == command.Name && c.DeliveryType == DeliveryType.Method)) + { + device.Commands.Add(command); + } + } + } + + /// + /// Convert Command to SupportedMethod. Array will be convert to an object since the Twin doesn't support array. + /// + /// + /// + private static SupportedMethod Convert(Command command) + { + var method = new SupportedMethod(); + method.Name = command.Name; + method.Description = command.Description; + command.Parameters.ForEach(p => method.Parameters.Add(p.Name, p)); + + return method; + } + + /// + /// Convert SupportedMethod back to Command. + /// + /// + /// + private static Command Convert(SupportedMethod method) + { + try + { + if (string.IsNullOrWhiteSpace(method.Name)) + { + throw new ArgumentNullException("Method Name"); + } + + var command = new Command(); + command.Name = method.Name; + command.Description = method.Description; + command.DeliveryType = DeliveryType.Method; + + foreach (var parameter in method.Parameters) + { + if (string.IsNullOrWhiteSpace(parameter.Value.Type)) + { + throw new ArgumentNullException("Parameter Type"); + } + + command.Parameters.Add(new Parameter(parameter.Key, parameter.Value.Type)); + } + + return command; + } + catch (Exception ex) + { + Trace.TraceWarning("Failed to covert supported method from reported property : {0}, message: {1}", JsonConvert.SerializeObject(method), ex.Message); + + return null; + } + } + + private static string NormalizeMethodName(Command command) + { + var parts = new List { command.Name.Replace("_", "__") }; + parts.AddRange(command.Parameters.Select(p => p.Type).ToList()); + + return string.Join("_", parts); + } + } +} diff --git a/Common/Models/SupportedMethod.cs b/Common/Models/SupportedMethod.cs new file mode 100644 index 00000000..68cba2bb --- /dev/null +++ b/Common/Models/SupportedMethod.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Models +{ + public class SupportedMethod + { + [JsonConstructor] + public SupportedMethod() + { + Parameters = new Dictionary(); + } + + public string Name { get; set; } + public Dictionary Parameters { get; set; } + public string Description { get; set; } + } +} diff --git a/DeviceAdministration/Infrastructure/BusinessLogic/DeviceLogic.cs b/DeviceAdministration/Infrastructure/BusinessLogic/DeviceLogic.cs index d45cf711..ec30f0b4 100644 --- a/DeviceAdministration/Infrastructure/BusinessLogic/DeviceLogic.cs +++ b/DeviceAdministration/Infrastructure/BusinessLogic/DeviceLogic.cs @@ -226,6 +226,8 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.DeviceAdmin.Infr // Get original device document DeviceModel existingDevice = await this.GetDeviceAsync(device.IoTHub.ConnectionDeviceId); + SupportedMethodsHelper.AddSupportedMethodsFromReportedProperty(device, existingDevice.Twin); + // Save the command history, original created date, and system properties (if any) of the existing device if (existingDevice.DeviceProperties != null) { diff --git a/Simulator/DMSimulator/iothub_client_sample_device_method.c b/Simulator/DMSimulator/iothub_client_sample_device_method.c index ec48dd5d..d26638b7 100644 --- a/Simulator/DMSimulator/iothub_client_sample_device_method.c +++ b/Simulator/DMSimulator/iothub_client_sample_device_method.c @@ -29,7 +29,7 @@ static bool g_continueRunning; #define DOWORK_LOOP_NUM 3 static char connectionString[1024] = { 0 }; -static char reportedProperties[1024] = { 0 }; +static char reportedProperties[4096] = { 0 }; typedef struct EVENT_INSTANCE_TAG { diff --git a/Simulator/Simulator.WebJob/SimulatorCore/Devices/DeviceBase.cs b/Simulator/Simulator.WebJob/SimulatorCore/Devices/DeviceBase.cs index b154d054..f5960e96 100644 --- a/Simulator/Simulator.WebJob/SimulatorCore/Devices/DeviceBase.cs +++ b/Simulator/Simulator.WebJob/SimulatorCore/Devices/DeviceBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Configurations; @@ -17,6 +18,7 @@ using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Simulator.WebJob.Sim using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Simulator.WebJob.SimulatorCore.Transport.Factory; using Microsoft.Azure.Devices.Common.Exceptions; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Simulator.WebJob.SimulatorCore.Devices { @@ -119,7 +121,7 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Simulator.WebJob { DeviceModel device = DeviceCreatorHelper.BuildDeviceStructure(DeviceID, true, null); device.DeviceProperties = this.DeviceProperties; - device.Commands = this.Commands ?? new List(); + device.Commands = this.Commands?.Where(c => c.DeliveryType == DeliveryType.Message).ToList() ?? new List(); device.Telemetry = this.Telemetry ?? new List(); device.Version = SampleDeviceFactory.VERSION_1_0; device.ObjectType = SampleDeviceFactory.OBJECT_TYPE_DEVICE_INFO; @@ -180,7 +182,11 @@ namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.Simulator.WebJob var deviceConnectionString = Client.IotHubConnectionStringBuilder.Create(HostName, authMethod).ToString(); // Device properties (InstalledRAM, Processor, etc.) should be treat as reported proerties - var reportedProperties = JsonConvert.SerializeObject(DeviceProperties, Formatting.Indented, new JsonSerializerSettings + // Supported methods should be treat as reported property + var jObject = JObject.FromObject(DeviceProperties); + jObject.Merge(JObject.FromObject(SupportedMethodsHelper.GenerateSupportedMethodsReportedProperty(Commands))); + + var reportedProperties = JsonConvert.SerializeObject(jObject, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new SkipByNameContractResolver("DeviceID") diff --git a/UnitTests/Common/SupportedMethodsHelperTests.cs b/UnitTests/Common/SupportedMethodsHelperTests.cs new file mode 100644 index 00000000..72488c30 --- /dev/null +++ b/UnitTests/Common/SupportedMethodsHelperTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Helpers; +using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Models; +using Microsoft.Azure.Devices.Applications.RemoteMonitoring.Common.Models.Commands; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.Azure.Devices.Applications.RemoteMonitoring.UnitTests.Common +{ + public class SupportedMethodsHelperTests + { + [Fact] + public void GenerateSupportedMethodsReportedPropertyTest() + { + var commands = new List() { + // Method with parameters + new Command("method1", DeliveryType.Method, "desc1", new List() { + new Parameter("p1", "string"), + new Parameter("p2", "int") + }), + // Command, should be ignored + new Command("command1", DeliveryType.Method, "desc1", new List() { + new Parameter("p1", "int"), + new Parameter("p2", "string") + }), + // Method without parameters + new Command("method2", DeliveryType.Method, "desc2"), + // Method name with _ + new Command("method_3", DeliveryType.Method, "desc3"), + // Method without name, should be ignored + new Command("", DeliveryType.Method, "desc2"), + // parameter with no type, should be ignored + new Command("method4", DeliveryType.Method, "desc1", new List() { + new Parameter("p1", ""), + new Parameter("p2", "int") + }), + }; + + var property = SupportedMethodsHelper.GenerateSupportedMethodsReportedProperty(commands); + + JObject supportedMethods = property["SupportedMethods"] as JObject; + Assert.Equal(supportedMethods.Count, commands.Where(c => c.DeliveryType == DeliveryType.Method).Count()); + + Assert.Equal(supportedMethods["method1_string_int"]["Name"].ToString(), "method1"); + Assert.Equal(supportedMethods["method1_string_int"]["Description"].ToString(), "desc1"); + Assert.Equal(supportedMethods["method1_string_int"]["Parameters"]["p1"]["Type"].ToString(), "string"); + Assert.Equal(supportedMethods["method1_string_int"]["Parameters"]["p2"]["Type"].ToString(), "int"); + + Assert.Equal(supportedMethods["method2"]["Name"].ToString(), "method2"); + Assert.Equal(supportedMethods["method2"]["Description"].ToString(), "desc2"); + + Assert.Equal(supportedMethods["method__3"]["Name"].ToString(), "method_3"); + + var device = new DeviceModel(); + var twin = new Twin(); + twin.Properties.Reported["SupportedMethods"] = supportedMethods; + + SupportedMethodsHelper.AddSupportedMethodsFromReportedProperty(device, twin); + Assert.Equal(supportedMethods.Count - 2, device.Commands.Count); + foreach(var command in device.Commands) + { + var srcCommand = commands.FirstOrDefault(c => c.Name == command.Name); + Assert.Equal(command.Name, srcCommand.Name); + Assert.Equal(command.Description, srcCommand.Description); + Assert.Equal(command.Parameters.Count, srcCommand.Parameters.Count); + + foreach (var parameter in command.Parameters) + { + var srcParameter = srcCommand.Parameters.FirstOrDefault(p => p.Name == parameter.Name); + Assert.Equal(parameter.Name, srcParameter.Name); + Assert.Equal(parameter.Type, srcParameter.Type); + } + } + } + } +} diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index efed97d1..481d1f24 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -202,6 +202,7 @@ +