diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Controllers/v1.0/DevicesControllerTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Controllers/v1.0/DevicesControllerTests.cs index 0bdcb4339..6365e3d35 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Controllers/v1.0/DevicesControllerTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Controllers/v1.0/DevicesControllerTests.cs @@ -11,7 +11,9 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Controllers.V10 using Azure; using Azure.Data.Tables; using AzureIoTHub.Portal.Server.Controllers.V10; + using AzureIoTHub.Portal.Server.Entities; using AzureIoTHub.Portal.Server.Factories; + using AzureIoTHub.Portal.Server.Helpers; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Server.Services; @@ -338,5 +340,333 @@ public async Task WhenDeviceNotExistGetEnrollmentCredentialsShouldReturnNotFound this.mockRepository.VerifyAll(); } + + [Test] + public async Task WhenDeviceNotExistGetPropertiesShouldReturnNotFound() + { + // Arrange + var devicesController = this.CreateDevicesController(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync((Twin)null); + + // Act + var response = await devicesController.GetProperties("aaa"); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task WhenDeviceDoesntHaveModelIdNotExistGetPropertiesShouldReturnBadRequest() + { + // Arrange + var devicesController = this.CreateDevicesController(); + var twin = new Twin(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync(twin); + + // Act + var response = await devicesController.GetProperties("aaa"); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task WhenDeviceNotExistSetPropertiesShouldReturnNotFound() + { + // Arrange + var devicesController = this.CreateDevicesController(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync((Twin)null); + + // Act + var response = await devicesController.SetProperties("aaa", null); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task WhenDeviceDoesntHaveModelIdNotExistSetPropertiesShouldReturnBadRequest() + { + // Arrange + var devicesController = this.CreateDevicesController(); + var twin = new Twin(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync(twin); + + // Act + var response = await devicesController.SetProperties("aaa", null); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task GetPropertiesShouldReturnDeviceProperties() + { + // Arrange + var devicesController = this.CreateDevicesController(); + var twin = new Twin(); + + DeviceHelper.SetTagValue(twin, "ModelId", "bbb"); + DeviceHelper.SetDesiredProperty(twin, "writable", "ccc"); + + var properties = new List(); + + var mockTableClient = this.mockRepository.Create(); + var mockResponse = this.mockRepository.Create(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync(twin); + + _ = this.mockTableClientFactory.Setup(c => c.GetDeviceTemplateProperties()) + .Returns(mockTableClient.Object); + + _ = mockTableClient.Setup(c => c.QueryAsync( + It.Is(x => x == $"PartitionKey eq 'bbb'"), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(AsyncPageable.FromPages(new[] + { + Page.FromValues(new[] + { + new DeviceModelProperty + { + RowKey = Guid.NewGuid().ToString(), + PartitionKey = "bbb", + IsWritable = true, + Name = "writable", + }, + new DeviceModelProperty + { + RowKey = Guid.NewGuid().ToString(), + PartitionKey = "bbb", + IsWritable = false, + Name = "notwritable", + } + }, null, mockResponse.Object) + })); + + // Act + var response = await devicesController.GetProperties("aaa"); + + // Assert + Assert.IsNotNull(response); + Assert.IsNotNull(response.Value); + + Assert.AreEqual(2, response.Value.Count()); + Assert.AreEqual("ccc", response.Value.Single(x => x.Name == "writable").Value); + Assert.IsNull(response.Value.Single(x => x.Name == "notwritable").Value); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task SetPropertiesShouldUpdateDesiredProperties() + { + // Arrange + var devicesController = this.CreateDevicesController(); + var twin = new Twin(); + + DeviceHelper.SetTagValue(twin, "ModelId", "bbb"); + + var properties = new List(); + + var mockTableClient = this.mockRepository.Create(); + var mockResponse = this.mockRepository.Create(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync(twin); + + _ = this.mockDeviceService.Setup(c => c.UpdateDeviceTwin( + It.Is(c => c == "aaa"), + It.Is(c => c == twin))) + .ReturnsAsync(twin); + + _ = this.mockTableClientFactory.Setup(c => c.GetDeviceTemplateProperties()) + .Returns(mockTableClient.Object); + + _ = mockTableClient.Setup(c => c.QueryAsync( + It.Is(x => x == $"PartitionKey eq 'bbb'"), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(AsyncPageable.FromPages(new[] + { + Page.FromValues(new[] + { + new DeviceModelProperty + { + RowKey = Guid.NewGuid().ToString(), + PartitionKey = "bbb", + IsWritable = true, + Name = "writable", + } + }, null, mockResponse.Object) + })); + + // Act + var response = await devicesController.SetProperties("aaa", new[] + { + new DevicePropertyValue + { + Name = "writable", + Value = "ccc" + } + }); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + Assert.AreEqual("ccc", twin.Properties.Desired["writable"].ToString()); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task WhenPropertyNotWrittableSetPropertiesShouldNotUpdateDesiredProperty() + { + // Arrange + var devicesController = this.CreateDevicesController(); + var twin = new Twin(); + + DeviceHelper.SetTagValue(twin, "ModelId", "bbb"); + + var properties = new List(); + + var mockTableClient = this.mockRepository.Create(); + var mockResponse = this.mockRepository.Create(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync(twin); + + _ = this.mockDeviceService.Setup(c => c.UpdateDeviceTwin( + It.Is(c => c == "aaa"), + It.Is(c => c == twin))) + .ReturnsAsync(twin); + + _ = this.mockTableClientFactory.Setup(c => c.GetDeviceTemplateProperties()) + .Returns(mockTableClient.Object); + + _ = mockTableClient.Setup(c => c.QueryAsync( + It.Is(x => x == $"PartitionKey eq 'bbb'"), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(AsyncPageable.FromPages(new[] + { + Page.FromValues(new[] + { + new DeviceModelProperty + { + RowKey = Guid.NewGuid().ToString(), + PartitionKey = "bbb", + IsWritable = false, + Name = "notwritable", + } + }, null, mockResponse.Object) + })); + + // Act + var response = await devicesController.SetProperties("aaa", new[] + { + new DevicePropertyValue + { + Name = "notwritable", + Value = "ccc" + } + }); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + Assert.IsFalse(twin.Properties.Desired.Contains("notwritable")); + + this.mockRepository.VerifyAll(); + } + + [Test] + public async Task WhenPropertyNotInModelSetPropertiesShouldNotUpdateDesiredProperty() + { + // Arrange + var devicesController = this.CreateDevicesController(); + var twin = new Twin(); + + DeviceHelper.SetTagValue(twin, "ModelId", "bbb"); + + var properties = new List(); + + var mockTableClient = this.mockRepository.Create(); + var mockResponse = this.mockRepository.Create(); + + _ = this.mockDeviceService.Setup(c => c.GetDeviceTwin("aaa")) + .ReturnsAsync(twin); + + _ = this.mockDeviceService.Setup(c => c.UpdateDeviceTwin( + It.Is(c => c == "aaa"), + It.Is(c => c == twin))) + .ReturnsAsync(twin); + + _ = this.mockTableClientFactory.Setup(c => c.GetDeviceTemplateProperties()) + .Returns(mockTableClient.Object); + + _ = mockTableClient.Setup(c => c.QueryAsync( + It.Is(x => x == $"PartitionKey eq 'bbb'"), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(AsyncPageable.FromPages(new[] + { + Page.FromValues(new[] + { + new DeviceModelProperty + { + RowKey = Guid.NewGuid().ToString(), + PartitionKey = "bbb", + IsWritable = true, + Name = "writable", + } + }, null, mockResponse.Object) + })); + + // Act + var response = await devicesController.SetProperties("aaa", new[] + { + new DevicePropertyValue + { + Name = "unknown", + Value = "eee" + } + }); + + // Assert + Assert.IsNotNull(response); + Assert.IsAssignableFrom(response.Result); + + Assert.IsFalse(twin.Properties.Desired.Contains("unknown")); + + this.mockRepository.VerifyAll(); + } } } diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Converters/StringToBoolConverterTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Converters/StringToBoolConverterTests.cs new file mode 100644 index 000000000..f9b34ee83 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Converters/StringToBoolConverterTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Converters +{ + using AzureIoTHub.Portal.Client.Converters; + using Moq; + using NUnit.Framework; + + [TestFixture] + public class StringToBoolConverterTests + { + private MockRepository mockRepository; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + + } + + private static StringToBoolConverter CreateStringToBoolConverter() + { + return new StringToBoolConverter(); + } + + [TestCase(true, "true")] + [TestCase(false, "false")] + [TestCase(null, null)] + public void GetShouldReturnStringRepresentationOfBool(bool? value, string expected) + { + // Arrange + var stringToBoolConverter = CreateStringToBoolConverter(); + + // Act + var result = stringToBoolConverter.Get(value); + + // Assert + Assert.AreEqual(expected, result); + this.mockRepository.VerifyAll(); + } + + [TestCase("true", true)] + [TestCase("false", false)] + [TestCase("True", true)] + [TestCase("False", false)] + [TestCase("TRUE", true)] + [TestCase("FALSE", false)] + [TestCase("1", null)] + [TestCase(null, null)] + public void SetShouldReturnStringRepresentationOfBool(string value, bool? expected) + { + // Arrange + var stringToBoolConverter = CreateStringToBoolConverter(); + + // Act + var result = stringToBoolConverter.Set(value); + + // Assert + Assert.AreEqual(expected, result); + this.mockRepository.VerifyAll(); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs index c93988192..5e4a00404 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/DevicesModels/CreateDeviceModelPageTests.cs @@ -8,7 +8,6 @@ namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages using System.Linq; using System.Net.Http; using System.Net.Http.Json; - using System.Threading.Tasks; using AzureIoTHub.Portal.Client.Pages.DeviceModels; using AzureIoTHub.Portal.Server.Tests.Unit.Helpers; using AzureIoTHub.Portal.Shared.Models; diff --git a/src/AzureIoTHub.Portal/Client/Converters/StringToBoolConverter.cs b/src/AzureIoTHub.Portal/Client/Converters/StringToBoolConverter.cs new file mode 100644 index 000000000..69cc6a781 --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Converters/StringToBoolConverter.cs @@ -0,0 +1,28 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Client.Converters +{ + using MudBlazor; + + public class StringToBoolConverter : BoolConverter + { + public StringToBoolConverter() + { + SetFunc = OnSet; + GetFunc = OnGet; + } + + private string OnGet(bool? value) => value?.ToString()?.ToLowerInvariant(); + + private bool? OnSet(string arg) + { + if (!bool.TryParse(arg, out var value)) + { + return null; + } + + return value; + } + } +} diff --git a/src/AzureIoTHub.Portal/Client/Extensions/EnumerableExtension.cs b/src/AzureIoTHub.Portal/Client/Extensions/EnumerableExtension.cs new file mode 100644 index 000000000..e550bc3b4 --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Extensions/EnumerableExtension.cs @@ -0,0 +1,21 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Client.Extensions +{ + using System.Collections.Generic; + using System.Linq; + + public static class EnumerableExtension + { + public static int Next(this IEnumerable source) + { + if (!source.Any()) + { + return 0; + } + + return source.Max() + 1; + } + } +} diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor index 23bb1126b..8ae496e06 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor @@ -87,38 +87,47 @@ Properties - Add property + Add property - @foreach (var item in this.Properties) - { - - - x.Order)) + { + + + - - - + + - - - + + + + + - @foreach (DevicePropertyType item in Enum.GetValues(typeof(DevicePropertyType))) - { + @foreach (DevicePropertyType item in Enum.GetValues(typeof(DevicePropertyType))) + { @item - } + } @@ -129,7 +138,7 @@ Remove - } + } } diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor index 7b8359025..7a7a9f6a8 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor @@ -79,10 +79,10 @@ else Properties - Add property + Add property - @foreach (var item in this.Properties) + @foreach (var item in this.Properties.OrderBy(x => x.Order)) { @@ -93,13 +93,22 @@ else For="@(()=> item.DisplayName)" Required="true" /> - + + + + DeviceModel) + Variant="Variant.Outlined" ToStringFunc="@(x => x.Name)" - For="@(()=> Model)" - ResetValueOnEmptyText="true" - Immediate="true" Clearable="true" - CoerceText="true" CoerceValue="false" /> + ResetValueOnEmptyText=true + Immediate=true + Clearable=true + CoerceText=true + CoerceValue=false /> @if (Device.ModelId == null && displayValidationErrorMessages) {

The Model is required.

@@ -72,16 +77,6 @@ For="@(()=> Device.DeviceName)" Required="true" />
- - @foreach (DeviceTag tag in TagList) - { - - - - } - Status @@ -96,6 +91,76 @@ The device cannot connect to the platform. } + + + Tags + + @foreach (DeviceTag tag in TagList) + { + + + + } + @if (!IsLoRa) + { + + Properties + + @foreach (var item in Properties.OrderBy(c => c.Order)) + { + switch (item.PropertyType) + { + case DevicePropertyType.Boolean: + + + + break; + case DevicePropertyType.Double: + + string.IsNullOrEmpty(c) || double.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.Float: + + string.IsNullOrEmpty(c) || float.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.Integer: + + string.IsNullOrEmpty(c) || int.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.Long: + + string.IsNullOrEmpty(c) || long.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.String: + + + + break; + } + } + }
@@ -122,20 +187,10 @@ private string ApiUrlBase => this.IsLoRa ? "/api/lorawan/devices" : "api/devices"; private DeviceDetailsValidator standardValidator = new DeviceDetailsValidator(); private LoRaDeviceDetailsValidator loraValidator = new LoRaDeviceDetailsValidator(); - private DeviceModel Model - { - get - { - return DeviceModelList.SingleOrDefault(x => x.ModelId == Device.ModelId); - } - set - { - ChangeModel(value); - } - } private LoRaDeviceModel loRaDeviceModel { get; set; } - private bool IsLoRa => this.Model?.SupportLoRaFeatures ?? false; + + private bool IsLoRa => DeviceModelList.SingleOrDefault(c => c.ModelId == Device.ModelId)?.SupportLoRaFeatures ?? false; [Parameter] public string DeviceID { get; set; } @@ -143,9 +198,9 @@ private DeviceDetails Device { get; set; } = new DeviceDetails(); private IEnumerable DeviceModelList { get; set; } = new List(); - + private DeviceModel DeviceModel; private IEnumerable TagList { get; set; } = new List(); - private Dictionary TagDict { get; set; } = new Dictionary(); + private List Properties = new List(); private bool displayValidationErrorMessages = false; @@ -159,11 +214,11 @@ // Gets the custom tags that can be set when creating a device TagList = await Http.GetFromJsonAsync>($"/api/settings/device-tags"); + foreach (DeviceTag tag in TagList) { - TagDict.Add(tag.Name, ""); + Device.Tags.Add(tag.Name, ""); } - Device.Tags = TagDict; } /// @@ -183,13 +238,6 @@ // Allows to display ValidationError messages for the MudAutocomplete field. displayValidationErrorMessages = true; - //if (IsLoRa) - //{ - // var loraValidation = this.loraValidator.Validate(this.Device as LoRaDeviceDetails); - - // loraValidation.Errors.ForEach(x => Console.WriteLine(x.ErrorMessage)); - //} - return; } @@ -202,6 +250,10 @@ result.EnsureSuccessStatusCode(); + await Http.PostAsJsonAsync($"{ApiUrlBase}/{DeviceID}/properties", Properties); + + result.EnsureSuccessStatusCode(); + // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {Device.DeviceID} has been successfully created!", Severity.Success); @@ -241,20 +293,39 @@ .Where(x => x.Name.StartsWith(value, StringComparison.InvariantCultureIgnoreCase)); } - private void ChangeModel(DeviceModel model) + private async Task ChangeModel(DeviceModel model) { + Properties.Clear(); + + Device = new DeviceDetails + { + DeviceID = Device.DeviceID, + ModelId = model?.ModelId, + ImageUrl = model?.ImageUrl, + DeviceName = Device.DeviceName, + IsEnabled = Device.IsEnabled, + Tags = Device.Tags + }; + + if (model == null) + { + return; + } + if (model?.SupportLoRaFeatures ?? false) { var device = new LoRaDeviceDetails { DeviceID = Device.DeviceID, + ModelId = model.ModelId, DeviceName = Device.DeviceName, - IsEnabled = Device.IsEnabled + IsEnabled = Device.IsEnabled, + Tags = Device.Tags }; Device = device; - Http.GetFromJsonAsync($"api/lorawan/models/{model.ModelId}") + await Http.GetFromJsonAsync($"api/lorawan/models/{model.ModelId}") .ContinueWith(c => { device.AppEUI = c.Result.AppEUI; @@ -264,17 +335,19 @@ } else { - Device = new DeviceDetails - { - DeviceID = Device.DeviceID, - DeviceName = Device.DeviceName, - IsEnabled = Device.IsEnabled - }; + await Http.GetFromJsonAsync>($"api/models/{model.ModelId}/properties") + .ContinueWith(c => + { + Properties.AddRange(c.Result.Select(x => new DevicePropertyValue + { + DisplayName = x.DisplayName, + IsWritable = x.IsWritable, + Name = x.Name, + Order = x.Order, + PropertyType = x.PropertyType + })); + }); } - - Device.ModelId = model?.ModelId; - Device.ImageUrl = model?.ImageUrl; - Device.Tags = TagDict; } } diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor index 4113dd8ad..075524459 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor @@ -1,6 +1,7 @@ @page "/devices/{DeviceID}" @using AzureIoTHub.Portal.Client.Pages.Devices.LoRaWAN @using AzureIoTHub.Portal.Client.Validators +@using AzureIoTHub.Portal.Shared.Models @using AzureIoTHub.Portal.Shared.Models.v10.Device @using AzureIoTHub.Portal.Shared.Models.v10.DeviceModel @using AzureIoTHub.Portal.Shared.Models.v10.LoRaWAN.LoRaDevice @@ -57,23 +58,13 @@ ReadOnly="true" HelperText="DeviceID must contain 16 hexadecimal characters (numbers from 0 to 9 and/or letters from A to F)" /> - + - - @foreach (DeviceTag tag in TagList) - { - - - - } - Status @@ -88,6 +79,81 @@ The device cannot connect to the platform. } + + Tags + + @foreach (DeviceTag tag in TagList) + { + + + + } + + @if (!IsLoRa) + { + + Properties + + @foreach (var item in Properties.OrderBy(c => c.Order)) + { + switch (item.PropertyType) + { + case DevicePropertyType.Boolean: + + + + break; + case DevicePropertyType.Double: + + string.IsNullOrEmpty(c) || double.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.Float: + + string.IsNullOrEmpty(c) || float.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.Integer: + + string.IsNullOrEmpty(c) || int.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.Long: + + string.IsNullOrEmpty(c) || long.TryParse(c, out var result)) + Clearable="true" /> + + break; + case DevicePropertyType.String: + + + + break; + } + + } + } @@ -130,11 +196,13 @@ private bool isLoaded = false; - private IEnumerable DeviceModelList { get; set; } = new List(); + private IEnumerable DeviceModelList { get; set; } = Array.Empty(); private IEnumerable Commands { get; set; } - private IEnumerable TagList { get; set; } = new List(); + private IEnumerable TagList { get; set; } = Array.Empty(); + + private IEnumerable Properties = Array.Empty(); protected override async Task OnInitializedAsync() { @@ -149,6 +217,8 @@ } TagList = await Http.GetFromJsonAsync>($"/api/settings/device-tags"); + Properties = await Http.GetFromJsonAsync>($"{ApiUrlBase}/{DeviceID}/properties"); + isLoaded = true; } @@ -186,6 +256,10 @@ result.EnsureSuccessStatusCode(); + await Http.PostAsJsonAsync($"{ApiUrlBase}/{DeviceID}/properties", Properties); + + result.EnsureSuccessStatusCode(); + // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {Device.DeviceID} has been successfully updated!", Severity.Success); @@ -219,7 +293,7 @@ DialogService.Show("Device Credentials", parameters); } - private bool CheckTags() + private bool CheckTags() { bool tagValidationError = false; @@ -232,4 +306,4 @@ } return tagValidationError; } -} \ No newline at end of file +} diff --git a/src/AzureIoTHub.Portal/Client/_Imports.razor b/src/AzureIoTHub.Portal/Client/_Imports.razor index 8396624cd..8541dfdcd 100644 --- a/src/AzureIoTHub.Portal/Client/_Imports.razor +++ b/src/AzureIoTHub.Portal/Client/_Imports.razor @@ -9,4 +9,6 @@ @using Microsoft.JSInterop @using AzureIoTHub.Portal.Client @using AzureIoTHub.Portal.Client.Shared +@using AzureIoTHub.Portal.Client.Converters +@using AzureIoTHub.Portal.Client.Extensions @using MudBlazor diff --git a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesController.cs b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesController.cs index 942dcdc4f..8079dfbff 100644 --- a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesController.cs +++ b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesController.cs @@ -3,7 +3,9 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 { + using AzureIoTHub.Portal.Server.Entities; using AzureIoTHub.Portal.Server.Factories; + using AzureIoTHub.Portal.Server.Helpers; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Server.Services; @@ -12,6 +14,7 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; [ApiController] @@ -20,6 +23,16 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 [ApiExplorerSettings(GroupName = "IoT Devices")] public class DevicesController : DevicesControllerBase { + /// + /// The table client factory. + /// + private readonly ITableClientFactory tableClientFactory; + + /// + /// The devices service. + /// + private readonly IDeviceService devicesService; + public DevicesController( ILogger logger, IDeviceService devicesService, @@ -29,7 +42,8 @@ public DevicesController( ITableClientFactory tableClientFactory) : base(logger, devicesService, deviceTagService, deviceTwinMapper, deviceProvisioningServiceManager, tableClientFactory) { - + this.devicesService = devicesService; + this.tableClientFactory = tableClientFactory; } /// @@ -96,5 +110,102 @@ public override Task> GetCredentials(string { return base.GetCredentials(deviceID); } + + /// + /// Gets the device credentials. + /// + /// The device identifier. + /// + [HttpGet("{deviceID}/properties", Name = "GET Device Properties")] + public async Task>> GetProperties(string deviceID) + { + var device = await this.devicesService.GetDeviceTwin(deviceID); + + if (device == null) + { + return this.NotFound(); + } + + var modelId = DeviceHelper.RetrieveTagValue(device, nameof(DeviceDetails.ModelId)); + + if (string.IsNullOrEmpty(modelId)) + { + return this.BadRequest("Device has no modelId tag value"); + } + + var items = this.tableClientFactory + .GetDeviceTemplateProperties() + .QueryAsync($"PartitionKey eq '{modelId}'"); + + var result = new List(); + + await foreach (var item in items) + { + string value = null; + + if (item.IsWritable && device.Properties.Desired.Contains(item.Name)) + { + value = device.Properties.Desired[item.Name].ToString(); + } + else if (device.Properties.Reported.Contains(item.Name)) + { + value = device.Properties.Reported[item.Name].ToString(); + } + + result.Add(new DevicePropertyValue + { + DisplayName = item.DisplayName, + IsWritable = item.IsWritable, + Name = item.Name, + PropertyType = item.PropertyType, + Value = value + }); + } + + return result; + } + + /// + /// Gets the device credentials. + /// + /// The device identifier. + /// + [HttpPost("{deviceID}/properties", Name = "POST Device Properties")] + public async Task>> SetProperties(string deviceID, IEnumerable values) + { + var device = await this.devicesService.GetDeviceTwin(deviceID); + + if (device == null) + { + return this.NotFound(); + } + + var modelId = DeviceHelper.RetrieveTagValue(device, nameof(DeviceDetails.ModelId)); + + if (string.IsNullOrEmpty(modelId)) + { + return this.BadRequest("Device has no modelId tag value"); + } + + var items = this.tableClientFactory + .GetDeviceTemplateProperties() + .QueryAsync($"PartitionKey eq '{modelId}'"); + + var result = new List(); + + await foreach (var item in items) + { + if (!item.IsWritable) + { + continue; + } + + device.Properties.Desired[item.Name] = values.FirstOrDefault(x => x.Name == item.Name)?.Value; + } + + _ = await this.devicesService.UpdateDeviceTwin(deviceID, device); + + return this.Ok(); + } } } diff --git a/src/AzureIoTHub.Portal/Server/Entities/DeviceModelProperty.cs b/src/AzureIoTHub.Portal/Server/Entities/DeviceModelProperty.cs index 2c4a6c7cc..b846ccff3 100644 --- a/src/AzureIoTHub.Portal/Server/Entities/DeviceModelProperty.cs +++ b/src/AzureIoTHub.Portal/Server/Entities/DeviceModelProperty.cs @@ -1,4 +1,4 @@ -// Copyright (c) CGI France. All rights reserved. +// Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace AzureIoTHub.Portal.Server.Entities @@ -24,6 +24,11 @@ public class DeviceModelProperty : EntityBase /// public bool IsWritable { get; set; } + /// + /// The property display order. + /// + public int Order { get; set; } + /// /// The device property type /// diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/Device/DevicePropertyValue.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/Device/DevicePropertyValue.cs new file mode 100644 index 000000000..33f0a5f50 --- /dev/null +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/Device/DevicePropertyValue.cs @@ -0,0 +1,13 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Shared.Models.v10.Device +{ + public class DevicePropertyValue : DeviceProperty + { + /// + /// The current property value. + /// + public string Value { get; set; } + } +} diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceProperty.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceProperty.cs index bade2100e..edc46dc6e 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceProperty.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceProperty.cs @@ -27,6 +27,12 @@ public class DeviceProperty [Required(ErrorMessage = "The property should indicate whether it's writable or not.")] public bool IsWritable { get; set; } + /// + /// The property display order. + /// + [Required(ErrorMessage = "The property should indicate whether it's writable or not.")] + public int Order { get; set; } + /// /// The device property type ///