diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DeviceDetailPageTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DeviceDetailPageTests.cs index 0a79a0814..5ec2e0baf 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DeviceDetailPageTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DeviceDetailPageTests.cs @@ -22,6 +22,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Pages.Devices using MudBlazor.Services; using NUnit.Framework; using UnitTests.Mocks; + using AzureIoTHub.Portal.Models.v10.LoRaWAN; + using AzureIoTHub.Portal.Client.Pages.DeviceModels; [TestFixture] public class DeviceDetailPageTests : BlazorUnitTest @@ -42,10 +44,10 @@ public override void Setup() this.mockDialogService = MockRepository.Create(); this.mockSnackbarService = MockRepository.Create(); - this.mockDeviceModelsClientService = MockRepository.Create(); - this.mockLoRaWanDeviceModelsClientService = MockRepository.Create(); this.mockDeviceTagSettingsClientService = MockRepository.Create(); this.mockDeviceClientService = MockRepository.Create(); + this.mockDeviceModelsClientService = MockRepository.Create(); + this.mockLoRaWanDeviceModelsClientService = MockRepository.Create(); this.mockLoRaWanDeviceClientService = MockRepository.Create(); _ = Services.AddSingleton(this.mockDialogService.Object); @@ -65,6 +67,64 @@ public override void Setup() this.mockNavigationManager = Services.GetRequiredService(); } + [Test] + public void ShouldLoadDeviceDetails() + { + // Arrange + var deviceId = Guid.NewGuid().ToString(); + var modelId = Guid.NewGuid().ToString(); + + _ = this.mockDeviceClientService + .Setup(service => service.GetDevice(deviceId)) + .ReturnsAsync(new DeviceDetails() { ModelId = modelId }); + + _ = this.mockDeviceClientService + .Setup(service => service.GetDeviceProperties(deviceId)) + .ReturnsAsync(new List()); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModel(modelId)) + .ReturnsAsync(new DeviceModel()); + + _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) + .ReturnsAsync(new List()); + + // Act + var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", deviceId)); + + // Assert + _ = cut.WaitForElement("#returnButton"); + } + + [Test] + public void ShouldLoadLoRaDeviceDetails() + { + + // Arrange + var deviceId = Guid.NewGuid().ToString(); + var modelId = Guid.NewGuid().ToString(); + + _ = this.mockLoRaWanDeviceClientService + .Setup(service => service.GetDevice(deviceId)) + .ReturnsAsync(new LoRaDeviceDetails() { ModelId = modelId }); + + _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModel(modelId)) + .ReturnsAsync(new LoRaDeviceModel()); + + _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(modelId)) + .ReturnsAsync(new List()); + + _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) + .ReturnsAsync(new List()); + + // Act + var cut = RenderComponent( + ComponentParameter.CreateParameter("DeviceID", deviceId), + ComponentParameter.CreateParameter(nameof(DeviceModelDetailPage.IsLoRa), true)); + + // Assert + _ = cut.WaitForElement("#returnButton"); + } + [Test] public void ReturnButtonMustNavigateToPreviousPage() { @@ -157,7 +217,7 @@ public void ClickOnSaveShouldPutDeviceDetails() // Act var cut = RenderComponent(ComponentParameter.CreateParameter("DeviceID", mockDeviceDetails.DeviceID)); - cut.WaitForAssertion(() => cut.Find($"#{nameof(DeviceModel.Name)}>b").InnerHtml.Should().NotBeEmpty()); + cut.WaitForAssertion(() => cut.Find($"#{nameof(DeviceModel.Name)}").InnerHtml.Should().NotBeEmpty()); var saveButton = cut.WaitForElement("#saveButton"); saveButton.Click(); diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs index 9c3412fd5..ffec1094e 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/DevicesModels/DeviceModelDetailsPageTests.cs @@ -385,8 +385,7 @@ private LoRaDeviceModel SetupMockLoRaWANDeviceModel(DeviceModelCommand[] command Name = this.mockModelId, Description = Guid.NewGuid().ToString(), IsBuiltin = false, - ImageUrl = new Uri($"http://fake.local/{this.mockModelId}"), - SupportLoRaFeatures = true + ImageUrl = new Uri($"http://fake.local/{this.mockModelId}") }; _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => @@ -412,8 +411,7 @@ private LoRaDeviceModel SetupMockLoRaWANDeviceModelThrowingException() Name = this.mockModelId, Description = Guid.NewGuid().ToString(), IsBuiltin = false, - ImageUrl = new Uri($"http://fake.local/{this.mockModelId}"), - SupportLoRaFeatures = true + ImageUrl = new Uri($"http://fake.local/{this.mockModelId}") }; _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/DeviceLayoutServiceTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/DeviceLayoutServiceTests.cs index 148f88f7a..2ed0cb4bb 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/DeviceLayoutServiceTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Services/DeviceLayoutServiceTests.cs @@ -74,7 +74,7 @@ public void DuplicateSharedDeviceShouldReturnDuplicatedLoraWanDevice() var appKey = Fixture.Create(); // Act - var result = this.deviceLayoutService.DuplicateSharedDevice(new LoRaDeviceDetails + var loraWanDevice = this.deviceLayoutService.DuplicateSharedDevice(new LoRaDeviceDetails { DeviceID = deviceId, DeviceName = deviceName, @@ -82,8 +82,6 @@ public void DuplicateSharedDeviceShouldReturnDuplicatedLoraWanDevice() }); // Assert - var loraWanDevice = (LoRaDeviceDetails) result; - _ = loraWanDevice.DeviceID.Should().BeEmpty(); _ = loraWanDevice.DeviceName.Should().Be($"{deviceName} - copy"); _ = loraWanDevice.AppKey.Should().BeEmpty(); @@ -109,7 +107,7 @@ public void ResetSharedDeviceShouldReturnNewDevice() var expectedDevice = new DeviceDetails(); // Act - var result = this.deviceLayoutService.ResetSharedDevice(); + var result = this.deviceLayoutService.ResetSharedDevice(); // Assert _ = result.Should().BeEquivalentTo(expectedDevice); @@ -129,7 +127,7 @@ public void ResetSharedDeviceShouldReturnNewDeviceWithExpectedTags() } // Act - var result = this.deviceLayoutService.ResetSharedDevice(expectedTags); + var result = this.deviceLayoutService.ResetSharedDevice(expectedTags); // Assert _ = result.Should().BeEquivalentTo(expectedDevice); @@ -142,36 +140,24 @@ public void ResetSharedDeviceModelShouldReturnNewDeviceModel() var expectedDeviceModel = new DeviceModel(); // Act - var result = this.deviceLayoutService.ResetSharedDeviceModel(); + var result = this.deviceLayoutService.ResetSharedDeviceModel(); // Assert _ = result.Should().BeEquivalentTo(expectedDeviceModel); } [Test] - public void GetSharedDeviceShouldReturnDevice() + public void GetSharedDeviceShouldReturnNull() { - // Arrange - var expectedDevice = new DeviceDetails(); - - // Act - var result = this.deviceLayoutService.GetSharedDevice(); - // Assert - _ = result.Should().BeEquivalentTo(expectedDevice); + Assert.IsNull(this.deviceLayoutService.GetSharedDevice()); } [Test] - public void GetSharedDeviceModelShouldReturnDeviceModel() + public void GetSharedDeviceModelShouldReturnNull() { - // Arrange - var expectedDeviceModel = new DeviceModel(); - - // Act - var result = this.deviceLayoutService.GetSharedDeviceModel(); - // Assert - _ = result.Should().BeEquivalentTo(expectedDeviceModel); + Assert.IsNull(this.deviceLayoutService.GetSharedDeviceModel()); } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/LoRaDeviceModelMapperTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/LoRaDeviceModelMapperTests.cs index 8fb58da3c..9395d7cb8 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/LoRaDeviceModelMapperTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Mappers/LoRaDeviceModelMapperTests.cs @@ -103,11 +103,11 @@ public void CreateDeviceModelStateUnderTestExpectedBehavior(bool isBuiltin, bool this.mockRepository.VerifyAll(); } - [TestCase(false, false)] - [TestCase(true, false)] - [TestCase(true, true)] - [TestCase(false, true)] - public void UpdateTableEntityStateUnderTestExpectedBehavior(bool isBuiltin, bool supportLora) + [TestCase(false)] + [TestCase(true)] + [TestCase(true)] + [TestCase(false)] + public void UpdateTableEntityStateUnderTestExpectedBehavior(bool isBuiltin) { // Arrange var loRaDeviceModelMapper = CreateLoRaDeviceModelMapper(); @@ -119,9 +119,7 @@ public void UpdateTableEntityStateUnderTestExpectedBehavior(bool isBuiltin, bool Description = Guid.NewGuid().ToString(), ImageUrl = new Uri("http://fake.local"), SensorDecoder = Guid.NewGuid().ToString(), - IsBuiltin = isBuiltin, - SupportLoRaFeatures = supportLora, - UseOTAA = true + IsBuiltin = isBuiltin }; // Act @@ -133,7 +131,7 @@ public void UpdateTableEntityStateUnderTestExpectedBehavior(bool isBuiltin, bool Assert.AreEqual(model.ModelId, entity.RowKey); Assert.AreEqual(model.Name, entity[nameof(LoRaDeviceModel.Name)]); Assert.AreEqual(model.Description, entity[nameof(LoRaDeviceModel.Description)]); - Assert.AreEqual(supportLora, entity[nameof(LoRaDeviceModel.SupportLoRaFeatures)]); + Assert.AreEqual(true, entity[nameof(LoRaDeviceModel.SupportLoRaFeatures)]); Assert.AreEqual(isBuiltin, entity[nameof(LoRaDeviceModel.IsBuiltin)]); Assert.AreEqual(model.SensorDecoder, entity[nameof(LoRaDeviceModel.SensorDecoder)]); this.mockRepository.VerifyAll(); diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor index bca4c4e40..ceacf4c19 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/CreateDeviceModelPage.razor @@ -5,6 +5,7 @@ @using AzureIoTHub.Portal.Models @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Models.v10.LoRaWAN +@using AzureIoTHub.Portal.Shared.Models @attribute [Authorize] @inject NavigationManager NavigationManager @@ -42,7 +43,7 @@ - + @@ -62,12 +63,12 @@ LoRa Device - - + + @if (IsLoRa) { The device is a LoRa Device. - + } } @@ -124,18 +125,18 @@ Required="true"> @foreach (DevicePropertyType item in Enum.GetValues(typeof(DevicePropertyType))) { - @item + @item } - - - - - - - - Remove - - + + + + + + + + Remove + + } @@ -148,9 +149,9 @@ @if (IsLoRa) { - + + Commands="Commands" /> } @@ -209,7 +210,7 @@ }; } - internal DeviceModel Model { get; set; } = new DeviceModel + internal IDeviceModel Model { get; set; } = new DeviceModel { ModelId = Guid.NewGuid().ToString() }; @@ -218,6 +219,26 @@ private MultipartFormDataContent content; private string imageDataUrl; + private bool CheckLoRaValidation() + { + if (IsLoRa && this.Model is LoRaDeviceModel loRaDeviceModel) + { + return !this.loraValidator.Validate(loRaDeviceModel).IsValid; + } + + return true; + } + + private bool CheckGeneralValidation() + { + if (!IsLoRa && this.Model is DeviceModel deviceModel) + { + return !this.standardValidator.Validate(deviceModel).IsValid; + } + + return CheckLoRaValidation(); + } + private void DeleteAvatar() { content = null; @@ -268,20 +289,20 @@ } // Check validation error in commands - foreach(var cmd in Commands) + foreach (var cmd in Commands) { if (!CommandValidator.Validate(cmd).IsValid) cmdValidationError = true; } } - if (!standardValidator.Validate(Model).IsValid - || (IsLoRa && (!this.loraValidator.Validate(this.Model as LoRaDeviceModel).IsValid || - duplicated || - cmdValidationError))) + if (!IsLoRa ? !standardValidator.Validate(Model as DeviceModel).IsValid : + (!this.loraValidator.Validate(this.Model as LoRaDeviceModel).IsValid + || duplicated + || cmdValidationError)) { Snackbar.Add("One or more validation errors occurred", Severity.Error); - + isProcessing = false; return; @@ -291,17 +312,13 @@ { if (IsLoRa) { - var loraDeviceModel = Model as LoRaDeviceModel; - - loraDeviceModel.SupportLoRaFeatures = true; - - await LoRaWanDeviceModelsClientService.CreateDeviceModel(loraDeviceModel); + await LoRaWanDeviceModelsClientService.CreateDeviceModel(Model as LoRaDeviceModel); await LoRaWanDeviceModelsClientService.SetDeviceModelCommands(Model.ModelId, Commands); } else { - await DeviceModelsClientService.CreateDeviceModel(Model); + await DeviceModelsClientService.CreateDeviceModel(Model as DeviceModel); await DeviceModelsClientService.SetDeviceModelModelProperties(Model.ModelId, Properties); } @@ -333,4 +350,4 @@ isProcessing = false; } } - } +} diff --git a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor index bb43300c7..bc8ded96d 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/DeviceModels/DeviceModelDetailPage.razor @@ -5,6 +5,7 @@ @using AzureIoTHub.Portal.Models @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Models.v10.LoRaWAN +@using AzureIoTHub.Portal.Shared.Models @attribute [Authorize] @inject NavigationManager NavigationManager @@ -48,7 +49,7 @@ - + @@ -137,7 +138,7 @@ @if (IsLoRa) { - + @@ -189,23 +190,17 @@ } } - private void SetLoRaDeviceModel() - { - Model = new LoRaDeviceModel(Model); - } + private void SetLoRaDeviceModel() => Model = new LoRaDeviceModel(Model); - private void SetStandardDeviceModel() - { - Model = new DeviceModel - { - ModelId = Model.ModelId, - Name = Model.Name, - IsBuiltin = Model.IsBuiltin, - Description = Model.Description - }; - } + private void SetStandardDeviceModel() => Model = new DeviceModel + { + ModelId = Model.ModelId, + Name = Model.Name, + IsBuiltin = Model.IsBuiltin, + Description = Model.Description + }; - private DeviceModel Model { get; set; } = new DeviceModel + private IDeviceModel Model { get; set; } = new DeviceModel { ModelId = Guid.NewGuid().ToString() }; @@ -214,6 +209,26 @@ private MultipartFormDataContent content; private string imageDataUrl; + private bool CheckLoRaValidation() + { + if (IsLoRa && this.Model is LoRaDeviceModel loRaDeviceModel) + { + return !this.loraValidator.Validate(loRaDeviceModel).IsValid; + } + + return true; + } + + private bool CheckGeneralValidation() + { + if (!IsLoRa && this.Model is DeviceModel deviceModel) + { + return !this.standardValidator.Validate(deviceModel).IsValid; + } + + return CheckLoRaValidation(); + } + protected override async Task OnInitializedAsync() { try @@ -297,11 +312,10 @@ } } - if (!standardValidator.Validate(Model).IsValid - || !propertiesValidator.Validate(Properties).IsValid - || (IsLoRa && (!this.loraValidator.Validate(this.Model as LoRaDeviceModel).IsValid + if (!IsLoRa ? !standardValidator.Validate(Model as DeviceModel).IsValid : + (!this.loraValidator.Validate(this.Model as LoRaDeviceModel).IsValid || duplicated - || cmdValidationError))) + || cmdValidationError)) { Snackbar.Add("One or more validation errors occurred", Severity.Error); @@ -324,7 +338,7 @@ } else { - await DeviceModelsClientService.UpdateDeviceModel(Model); + await DeviceModelsClientService.UpdateDeviceModel(Model as DeviceModel); await DeviceModelsClientService.SetDeviceModelModelProperties(Model.ModelId, Properties); } diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor index 2e1273d0e..7fcf45926 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor @@ -5,6 +5,7 @@ @using AzureIoTHub.Portal.Models @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Models.v10.LoRaWAN +@using AzureIoTHub.Portal.Shared.Models @using Microsoft.AspNetCore.Components @attribute [Authorize] @@ -27,56 +28,56 @@ @(string.IsNullOrEmpty(Device.DeviceName) ? Device.DeviceID : Device.DeviceName) - - - -
- -
-
- - - - @saveButtonText - - Save - Save and add new - Save and duplicate - - + + + +
+ +
+
+ + + + @saveButtonText + + Save + Save and add new + Save and duplicate + + +
-
- - - - - - - - Details - - - + + + + + + + + Details + + + @if (duplicateDevice) { } else { - this.DeviceModel) - Variant="Variant.Outlined" - ToStringFunc="@(x => x?.Name)" - ResetValueOnEmptyText=true - Immediate=true - Clearable=true - CoerceText=true - CoerceValue=false> + this.DeviceModel) + Variant="Variant.Outlined" + ToStringFunc="@(x => x?.Name)" + ResetValueOnEmptyText=true + Immediate=true + Clearable=true + CoerceText=true + CoerceValue=false> @context.Name @@ -94,23 +95,23 @@ @if (IsLoRa) { + id=@nameof(LoRaDeviceDetails.DeviceID) + Label="Device ID / DevEUI" + Variant="Variant.Outlined" + Validation=@(loraValidator.ValidateValue) + For="@(()=> Device.DeviceID)" + Mask="@maskLoRaDeviceID" + HelperText="Device EUI must contain 16 hexadecimal characters (numbers from 0 to 9 and/or letters from A to F)" /> } else { + id=@nameof(DeviceDetails.DeviceID) + Label="Device ID" + Variant="Variant.Outlined" + Validation=@(standardValidator.ValidateValue) + For="@(()=> Device.DeviceID)" + HelperText="The device identifier should be of ASCII 7-bit alphanumeric characters plus certain special characters" /> } @@ -233,7 +234,7 @@ @if (IsLoRa) { - + } @@ -244,31 +245,37 @@ @code { [CascadingParameter] - public Error Error {get; set;} + public Error Error { get; set; } - private DeviceDetails Device; + private IDeviceDetails Device = new DeviceDetails(); private MudForm form; - + private DeviceDetailsValidator standardValidator = new DeviceDetailsValidator(); private LoRaDeviceDetailsValidator loraValidator = new LoRaDeviceDetailsValidator(); private LoRaDeviceModel loRaDeviceModel { get; set; } - private bool IsLoRa => DeviceModel?.SupportLoRaFeatures ?? false; + private bool IsLoRa + { + get + { + return Device is LoRaDeviceDetails; + } + } public PatternMask maskLoRaDeviceID = new PatternMask("XXXXXXXXXXXXXXXX") - { + { MaskChars = new[] { new MaskChar('X', @"[0-9a-fA-F]") }, CleanDelimiters = false, Transformation = AllUpperCase - }; + }; private static char AllUpperCase(char c) => c.ToString().ToUpperInvariant()[0]; - private IEnumerable DeviceModelList { get; set; } = new List(); - private DeviceModel _deviceModel; + private IEnumerable DeviceModelList { get; set; } = new List(); + private IDeviceModel _deviceModel; - private DeviceModel DeviceModel + private IDeviceModel DeviceModel { get => _deviceModel; set { Task.Run(async () => await ChangeModel(value)); } @@ -290,8 +297,8 @@ try { DeviceLayoutService.RefreshDeviceOccurred += DeviceServiceOnRefreshDeviceOccurred; - Device = DeviceLayoutService.GetSharedDevice(); - DeviceModel = DeviceLayoutService.GetSharedDeviceModel(); + Device = DeviceLayoutService.GetSharedDevice() ?? this.Device; + DeviceModel = DeviceLayoutService.GetSharedDeviceModel() ?? this.DeviceModel; // Enable device by default Device.IsEnabled = true; @@ -315,8 +322,8 @@ public void Dispose() { - DeviceLayoutService.ResetSharedDevice(); - DeviceLayoutService.ResetSharedDeviceModel(); + DeviceLayoutService.ResetSharedDevice(); + DeviceLayoutService.ResetSharedDeviceModel(); DeviceLayoutService.RefreshDeviceOccurred -= DeviceServiceOnRefreshDeviceOccurred; } @@ -330,11 +337,8 @@ isProcessing = true; await form.Validate(); - bool tagValidationError = CheckTagsError(); - if (!standardValidator.Validate(Device).IsValid - || (IsLoRa && !this.loraValidator.Validate(this.Device as LoRaDeviceDetails).IsValid) - || tagValidationError) + if (CheckTagsError() || CheckGeneralValidation() || CheckLoRaValidation()) { Snackbar.Add("One or more validation errors occurred", Severity.Error); @@ -350,7 +354,7 @@ await LoRaWanDeviceClientService.CreateDevice(Device as LoRaDeviceDetails); else { - await DeviceClientService.CreateDevice(Device); + await DeviceClientService.CreateDevice(Device as DeviceDetails); await DeviceClientService.SetDeviceProperties(Device.DeviceID, Properties); } @@ -377,11 +381,11 @@ NavManager.NavigateTo("devices"); break; case DeviceSaveAction.SaveAndAddNew: - Device = DeviceLayoutService.ResetSharedDevice(TagList.ToList()); - DeviceModel = DeviceLayoutService.ResetSharedDeviceModel(); + Device = DeviceLayoutService.ResetSharedDevice(TagList.ToList()); + DeviceModel = DeviceLayoutService.ResetSharedDeviceModel(); break; case DeviceSaveAction.SaveAndDuplicate: - Device = DeviceLayoutService.DuplicateSharedDevice(Device); + Device = DeviceLayoutService.DuplicateSharedDevice(Device as DeviceDetails); DeviceModel = DeviceLayoutService.DuplicateSharedDeviceModel(DeviceModel); break; } @@ -408,12 +412,37 @@ return tagValidationError; } + private bool CheckLoRaValidation() + { + if (!IsLoRa) + { + return false; + } + + if (this.Device is LoRaDeviceDetails loRaDeviceDetails) + { + return !this.loraValidator.Validate(loRaDeviceDetails).IsValid; + } + + return true; + } + + private bool CheckGeneralValidation() + { + if (!IsLoRa && this.Device is DeviceDetails deviceDetails) + { + return !this.standardValidator.Validate(deviceDetails).IsValid; + } + + return CheckLoRaValidation(); + } + /// /// Allows to autocomplete the Device Model field in the form. /// /// Text entered in the field /// Item of the device model list that matches the user's value - private async Task> Search(string value) + private async Task> Search(string value) { // In real life use an asynchronous function for fetching data from an api. await Task.Delay(0); @@ -438,9 +467,9 @@ }; } - - internal async Task ChangeModel(DeviceModel model) + + internal async Task ChangeModel(IDeviceModel model) { try { @@ -449,38 +478,36 @@ _deviceModel = model; Device = new DeviceDetails - { - DeviceID = Device.DeviceID, - ModelId = model?.ModelId, - ImageUrl = model?.ImageUrl, - DeviceName = Device.DeviceName, - IsEnabled = Device.IsEnabled, - Tags = Device.Tags - }; + { + DeviceID = Device.DeviceID, + ModelId = model?.ModelId, + ImageUrl = model?.ImageUrl, + DeviceName = Device.DeviceName, + IsEnabled = Device.IsEnabled, + Tags = Device.Tags + }; if (model == null || string.IsNullOrWhiteSpace(model.ModelId)) { return; } - if (model?.SupportLoRaFeatures ?? false) + if (model.SupportLoRaFeatures) { - var device = new LoRaDeviceDetails - { - DeviceID = this.Device.DeviceID, - ModelId = model.ModelId, - ImageUrl = model.ImageUrl, - DeviceName = this.Device.DeviceName, - IsEnabled = this.Device.IsEnabled, - Tags = this.Device.Tags - }; - - this.Device = device; - var loRaDeviceModelResult = await LoRaWanDeviceModelsClientService.GetDeviceModel(model.ModelId); - device.SensorDecoder = loRaDeviceModelResult.SensorDecoder; - device.UseOTAA = loRaDeviceModelResult.UseOTAA; + this.Device = new LoRaDeviceDetails + { + DeviceID = this.Device.DeviceID, + ModelId = model.ModelId, + ImageUrl = model.ImageUrl, + DeviceName = this.Device.DeviceName, + IsEnabled = this.Device.IsEnabled, + Tags = this.Device.Tags, + SensorDecoder = loRaDeviceModelResult.SensorDecoder, + UseOTAA = loRaDeviceModelResult.UseOTAA + }; + loRaDeviceModel = loRaDeviceModelResult; } else @@ -488,13 +515,13 @@ var properties = await DeviceModelsClientService.GetDeviceModelModelProperties(model.ModelId); Properties.AddRange(properties.Select(x => new DevicePropertyValue - { - DisplayName = x.DisplayName, - IsWritable = x.IsWritable, - Name = x.Name, - Order = x.Order, - PropertyType = x.PropertyType - })); + { + DisplayName = x.DisplayName, + IsWritable = x.IsWritable, + Name = x.Name, + Order = x.Order, + PropertyType = x.PropertyType + })); } } catch (ProblemDetailsException exception) diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor index 8fac40d08..84bb4eafb 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor @@ -4,12 +4,12 @@ @using AzureIoTHub.Portal.Models @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Models.v10.LoRaWAN +@using AzureIoTHub.Portal.Shared.Models @attribute [Authorize] -@inject ISnackbar Snackbar @inject NavigationManager NavManager +@inject ISnackbar Snackbar @inject IDialogService DialogService -@inject NavigationManager navigationManager @inject IDeviceModelsClientService DeviceModelsClientService @inject ILoRaWanDeviceModelsClientService LoRaWanDeviceModelsClientService @inject IDeviceTagSettingsClientService DeviceTagSettingsClientService @@ -61,7 +61,7 @@ - + @@ -201,7 +201,7 @@ @if (IsLoRa && Device != null && Commands != null) { - + } @@ -229,9 +229,9 @@ [Parameter] public string DeviceID { get; set; } - private DeviceDetails Device { get; set; } = new DeviceDetails(); + private IDeviceDetails Device { get; set; } = new DeviceDetails(); - private DeviceModel DeviceModel { get;set; } = new DeviceModel(); + private IDeviceModel DeviceModel { get; set; } = new DeviceModel(); private bool isLoaded = false; @@ -240,7 +240,7 @@ private DeviceSaveAction deviceSaveAction = DeviceSaveAction.Save; private string saveButtonText = "Save"; - private void Return() => navigationManager.NavigateTo("devices"); + private void Return() => NavManager.NavigateTo("devices"); private IEnumerable Commands { get; set; } @@ -303,11 +303,8 @@ isProcessing = true; await form.Validate(); - bool tagValidationError = CheckTagsError(); - if (!standardValidator.Validate(Device).IsValid - || (IsLoRa && !this.loraValidator.Validate(this.Device as LoRaDeviceDetails).IsValid) - || tagValidationError) + if (CheckTagsError() || CheckGeneralValidation() || CheckLoRaValidation()) { Snackbar.Add("One or more validation errors occurred", Severity.Error); @@ -322,7 +319,7 @@ } else { - await DeviceClientService.UpdateDevice(Device); + await DeviceClientService.UpdateDevice(Device as DeviceDetails); await DeviceClientService.SetDeviceProperties(DeviceID, Properties.ToList()); } @@ -365,6 +362,31 @@ return tagValidationError; } + private bool CheckLoRaValidation() + { + if(!IsLoRa) + { + return false; + } + + if (this.Device is LoRaDeviceDetails loRaDeviceDetails) + { + return !this.loraValidator.Validate(loRaDeviceDetails).IsValid; + } + + return true; + } + + private bool CheckGeneralValidation() + { + if (!IsLoRa && this.Device is DeviceDetails deviceDetails) + { + return !this.standardValidator.Validate(deviceDetails).IsValid; + } + + return CheckLoRaValidation(); + } + /// /// Prompts a pop-up windows to confirm the device's deletion. /// diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor index 4ff950a4d..942141c9c 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor @@ -1,4 +1,5 @@ @using AzureIoTHub.Portal.Models.v10 +@using AzureIoTHub.Portal.Shared.Models @using Microsoft.AspNetCore.Components @using System.Web @@ -64,8 +65,8 @@ { try { - DeviceDetails device; - DeviceModel deviceModel; + IDeviceDetails device; + IDeviceModel deviceModel; if (deviceToDuplicate.SupportLoRaFeatures) { @@ -77,8 +78,8 @@ device = await DeviceClientService.GetDevice(deviceToDuplicate.DeviceID); deviceModel = await DeviceModelsClientService.GetDeviceModel(device.ModelId); } - DeviceLayoutService.DuplicateSharedDevice(device); - DeviceLayoutService.DuplicateSharedDeviceModel(deviceModel); + DeviceLayoutService.DuplicateSharedDevice(device as DeviceDetails); + DeviceLayoutService.DuplicateSharedDeviceModel(deviceModel as DeviceModel); DeviceLayoutService.RefreshDevice(); } catch (ProblemDetailsException exception) diff --git a/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs b/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs index 698e79d62..f98e5f082 100644 --- a/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs +++ b/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs @@ -5,13 +5,14 @@ namespace AzureIoTHub.Portal.Client.Services { using System; using System.Collections.Generic; + using AzureIoTHub.Portal.Shared.Models; using Portal.Models.v10; using Portal.Models.v10.LoRaWAN; public class DeviceLayoutService : IDeviceLayoutService { - private DeviceDetails sharedDevice = new(); - private DeviceModel sharedDeviceModel = new(); + private IDeviceDetails sharedDevice; + private IDeviceModel sharedDeviceModel; public event EventHandler RefreshDeviceOccurred; @@ -20,60 +21,62 @@ public void RefreshDevice() OnRefreshDeviceOccurred(); } - public DeviceDetails GetSharedDevice() + public IDeviceDetails GetSharedDevice() { return this.sharedDevice; } - public DeviceModel GetSharedDeviceModel() + public IDeviceModel GetSharedDeviceModel() { return this.sharedDeviceModel; } - public DeviceDetails ResetSharedDevice(List tags = null) + public TDevice ResetSharedDevice(List tags = null) + where TDevice : class, IDeviceDetails, new() { - this.sharedDevice = new DeviceDetails(); + this.sharedDevice = new TDevice(); foreach (var tag in tags ?? new List()) { _ = this.sharedDevice.Tags.TryAdd(tag.Name, string.Empty); } - return this.sharedDevice; + return this.sharedDevice as TDevice; } - public DeviceModel ResetSharedDeviceModel() + public TDeviceModel ResetSharedDeviceModel() + where TDeviceModel : class, IDeviceModel, new() { - this.sharedDeviceModel = new DeviceModel(); + this.sharedDeviceModel = new TDeviceModel(); - return this.sharedDeviceModel; + return this.sharedDeviceModel as TDeviceModel; } - public DeviceDetails DuplicateSharedDevice(DeviceDetails deviceToDuplicate) + public TDevice DuplicateSharedDevice(TDevice deviceToDuplicate) + where TDevice : IDeviceDetails { deviceToDuplicate.DeviceID = string.Empty; deviceToDuplicate.DeviceName = $"{deviceToDuplicate.DeviceName} - copy"; - if (deviceToDuplicate.IsLoraWan) + if (deviceToDuplicate is LoRaDeviceDetails loRaDevice) { - var loRaDeviceDetails = (LoRaDeviceDetails)deviceToDuplicate; - loRaDeviceDetails.AppKey = string.Empty; - - this.sharedDevice = loRaDeviceDetails; + loRaDevice.AppKey = string.Empty; + this.sharedDevice = loRaDevice; } else { this.sharedDevice = deviceToDuplicate; } - return this.sharedDevice; + return (TDevice)this.sharedDevice; } - public DeviceModel DuplicateSharedDeviceModel(DeviceModel deviceModelToDuplicate) + public TDeviceModel DuplicateSharedDeviceModel(TDeviceModel deviceModelToDuplicate) + where TDeviceModel : IDeviceModel { this.sharedDeviceModel = deviceModelToDuplicate; - return this.sharedDeviceModel; + return (TDeviceModel)this.sharedDeviceModel; } private void OnRefreshDeviceOccurred() => RefreshDeviceOccurred?.Invoke(this, EventArgs.Empty); diff --git a/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs b/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs index 313ed5219..6b17713cc 100644 --- a/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs +++ b/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs @@ -5,6 +5,7 @@ namespace AzureIoTHub.Portal.Client.Services { using System; using System.Collections.Generic; + using AzureIoTHub.Portal.Shared.Models; using Portal.Models.v10; public interface IDeviceLayoutService @@ -13,11 +14,15 @@ public interface IDeviceLayoutService void RefreshDevice(); - DeviceDetails GetSharedDevice(); - DeviceModel GetSharedDeviceModel(); - DeviceDetails ResetSharedDevice(List tags = null); - DeviceModel ResetSharedDeviceModel(); - DeviceDetails DuplicateSharedDevice(DeviceDetails deviceToDuplicate); - DeviceModel DuplicateSharedDeviceModel(DeviceModel deviceModelToDuplicate); + IDeviceDetails GetSharedDevice(); + IDeviceModel GetSharedDeviceModel(); + TDevice ResetSharedDevice(List tags = null) + where TDevice : class, IDeviceDetails, new(); + TDeviceModel ResetSharedDeviceModel() + where TDeviceModel : class, IDeviceModel, new(); + TDevice DuplicateSharedDevice(TDevice deviceToDuplicate) + where TDevice : IDeviceDetails; + TDeviceModel DuplicateSharedDeviceModel(TDeviceModel deviceModelToDuplicate) + where TDeviceModel : IDeviceModel; } } diff --git a/src/AzureIoTHub.Portal/Client/Services/ILoRaWanDeviceClientService.cs b/src/AzureIoTHub.Portal/Client/Services/ILoRaWanDeviceClientService.cs index 81ef98924..7ac7b39de 100644 --- a/src/AzureIoTHub.Portal/Client/Services/ILoRaWanDeviceClientService.cs +++ b/src/AzureIoTHub.Portal/Client/Services/ILoRaWanDeviceClientService.cs @@ -4,7 +4,7 @@ namespace AzureIoTHub.Portal.Client.Services { using System.Threading.Tasks; - using Portal.Models.v10.LoRaWAN; + using AzureIoTHub.Portal.Models.v10.LoRaWAN; public interface ILoRaWanDeviceClientService { diff --git a/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceDetailsValidator.cs b/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceDetailsValidator.cs index 20b429ce7..d67d6ecd0 100644 --- a/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceDetailsValidator.cs +++ b/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceDetailsValidator.cs @@ -3,8 +3,12 @@ namespace AzureIoTHub.Portal.Client.Validators { + using System.Collections.Generic; + using System.Threading.Tasks; + using System; using AzureIoTHub.Portal.Models.v10.LoRaWAN; using FluentValidation; + using System.Linq; public class LoRaDeviceDetailsValidator : AbstractValidator { @@ -43,5 +47,15 @@ public LoRaDeviceDetailsValidator() .When(x => !x.UseOTAA) .WithMessage("DevAddr is required."); } + + public Func>> ValidateValue => async (model, propertyName) => + { + var result = await ValidateAsync(ValidationContext.CreateWithOptions((LoRaDeviceDetails)model, x => x.IncludeProperties(propertyName))); + + if (result.IsValid) + return Array.Empty(); + + return result.Errors.Select(e => e.ErrorMessage); + }; } } diff --git a/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceModelValidator.cs b/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceModelValidator.cs index 9799b88d4..0a882d9ee 100644 --- a/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceModelValidator.cs +++ b/src/AzureIoTHub.Portal/Client/Validators/LoRaDeviceModelValidator.cs @@ -3,10 +3,10 @@ namespace AzureIoTHub.Portal.Client.Validators { - using AzureIoTHub.Portal.Models.v10.LoRaWAN; + using AzureIoTHub.Portal.Shared.Models; using FluentValidation; - public class LoRaDeviceModelValidator : AbstractValidator + public class LoRaDeviceModelValidator : AbstractValidator { } } diff --git a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelControllerBase.cs b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelControllerBase.cs index d3d4d980d..0a08b9e18 100644 --- a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelControllerBase.cs +++ b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DeviceModelControllerBase.cs @@ -13,16 +13,16 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 using AzureIoTHub.Portal.Server.Helpers; using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Services; - using AzureIoTHub.Portal.Models.v10; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Devices.Shared; using Microsoft.Extensions.Logging; using AzureIoTHub.Portal.Server.Exceptions; + using AzureIoTHub.Portal.Shared.Models; public abstract class DeviceModelsControllerBase : ControllerBase - where TListItemModel : DeviceModel - where TModel : DeviceModel + where TListItemModel : class, IDeviceModel + where TModel : class, IDeviceModel { #pragma warning disable RCS1158 // Static member in generic type should use a type parameter. /// diff --git a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesControllerBase.cs b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesControllerBase.cs index 247ae155e..e98263372 100644 --- a/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesControllerBase.cs +++ b/src/AzureIoTHub.Portal/Server/Controllers/v1.0/DevicesControllerBase.cs @@ -16,6 +16,7 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 using AzureIoTHub.Portal.Server.Managers; using AzureIoTHub.Portal.Server.Mappers; using AzureIoTHub.Portal.Server.Services; + using AzureIoTHub.Portal.Shared.Models; using Exceptions; using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Http; @@ -27,7 +28,7 @@ namespace AzureIoTHub.Portal.Server.Controllers.V10 public abstract class DevicesControllerBase : ControllerBase where TListItem : DeviceListItem - where TModel : DeviceDetails + where TModel : IDeviceDetails { private readonly IDeviceService devicesService; private readonly IDeviceTagService deviceTagService; diff --git a/src/AzureIoTHub.Portal/Server/Mappers/IDeviceModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/IDeviceModelMapper.cs index 36d2b9e48..a540eb9a5 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/IDeviceModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/IDeviceModelMapper.cs @@ -4,12 +4,12 @@ namespace AzureIoTHub.Portal.Server.Managers { using Azure.Data.Tables; - using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Shared.Models; using System.Collections.Generic; public interface IDeviceModelMapper - where TListItem : DeviceModel - where TModel : DeviceModel + where TListItem : class, IDeviceModel + where TModel : class, IDeviceModel { public TListItem CreateDeviceModelListItem(TableEntity entity); diff --git a/src/AzureIoTHub.Portal/Server/Mappers/IDeviceTwinMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/IDeviceTwinMapper.cs index 74b9274d0..72c446f6e 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/IDeviceTwinMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/IDeviceTwinMapper.cs @@ -4,12 +4,13 @@ namespace AzureIoTHub.Portal.Server.Mappers { using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Shared.Models; using Microsoft.Azure.Devices.Shared; using System.Collections.Generic; public interface IDeviceTwinMapper where TListItem : DeviceListItem - where TDevice : DeviceDetails + where TDevice : IDeviceDetails { TDevice CreateDeviceDetails(Twin twin, IEnumerable tags); diff --git a/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceMapper.cs index b65586444..930ef96ab 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceMapper.cs @@ -54,7 +54,7 @@ public LoRaDeviceDetails CreateDeviceDetails(Twin twin, IEnumerable tags AppSKey = Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.AppSKey)), NwkSKey = Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.NwkSKey)), Deduplication = Enum.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.Deduplication)), out var deduplication) ? deduplication : DeduplicationMode.None, - PreferredWindow = int.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.PreferredWindow)), out var preferedWindow) ? preferedWindow : null, + PreferredWindow = int.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.PreferredWindow)), out var preferedWindow) ? preferedWindow : default, Supports32BitFCnt = bool.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.Supports32BitFCnt)), out var boolResult) ? boolResult : null, ABPRelaxMode = bool.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.ABPRelaxMode)), out boolResult) ? boolResult : null, KeepAliveTimeout = int.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.KeepAliveTimeout)), out var keepAliveTimeout) ? keepAliveTimeout : null, diff --git a/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceModelMapper.cs b/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceModelMapper.cs index 4e48d084e..cb248c35a 100644 --- a/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceModelMapper.cs +++ b/src/AzureIoTHub.Portal/Server/Mappers/LoRaDeviceModelMapper.cs @@ -48,7 +48,6 @@ public LoRaDeviceModel CreateDeviceModel(TableEntity entity) Name = entity[nameof(LoRaDeviceModel.Name)]?.ToString(), Description = entity[nameof(LoRaDeviceModel.Description)]?.ToString(), SensorDecoder = entity[nameof(LoRaDeviceModel.SensorDecoder)]?.ToString(), - SupportLoRaFeatures = true, UseOTAA = bool.Parse(entity[nameof(LoRaDeviceModel.UseOTAA)]?.ToString() ?? "true"), PreferredWindow = int.TryParse(entity[nameof(LoRaDeviceModel.PreferredWindow)]?.ToString(), out var intResult) ? intResult : 1, Supports32BitFCnt = bool.TryParse(entity[nameof(LoRaDeviceModel.Supports32BitFCnt)]?.ToString(), out var boolResult) ? boolResult : null, diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceDetails.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceDetails.cs index 9ad9c5a3a..5b2e3c389 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceDetails.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceDetails.cs @@ -6,11 +6,12 @@ namespace AzureIoTHub.Portal.Models.v10 using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; + using AzureIoTHub.Portal.Shared.Models; /// /// Device details. /// - public class DeviceDetails + public class DeviceDetails : IDeviceDetails { /// /// The device identifier. @@ -60,6 +61,6 @@ public class DeviceDetails /// /// true if this instance is lorawan; otherwise, false. /// - public virtual bool IsLoraWan { get; private set; } + public virtual bool IsLoraWan { get; } } } diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs index 04af1a7b1..ae5724591 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/DeviceModel.cs @@ -5,11 +5,12 @@ namespace AzureIoTHub.Portal.Models.v10 { using System; using System.ComponentModel.DataAnnotations; + using AzureIoTHub.Portal.Shared.Models; /// /// Device model. /// - public class DeviceModel + public class DeviceModel : IDeviceModel { /// /// The device model identifier. diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/IDeviceDetails.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/IDeviceDetails.cs new file mode 100644 index 000000000..e09eb90c1 --- /dev/null +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/IDeviceDetails.cs @@ -0,0 +1,51 @@ +// 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 +{ + using System; + using System.Collections.Generic; + + public interface IDeviceDetails + { + /// + /// The device identifier. + /// + public string DeviceID { get; set; } + + /// + /// The name of the device. + /// + public string DeviceName { get; set; } + + /// + /// The model identifier. + /// + public string ModelId { get; set; } + + /// + /// The device model image Url. + /// + public Uri ImageUrl { get; set; } + + /// + /// true if this instance is connected; otherwise, false. + /// + public bool IsConnected { get; set; } + + /// + /// true if this instance is enabled; otherwise, false. + /// + public bool IsEnabled { get; set; } + + /// + /// The status updated time. + /// + public DateTime StatusUpdatedTime { get; set; } + + /// + /// List of custom device tags and their values. + /// + public Dictionary Tags { get; set; } + } +} diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/IDeviceModel.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/IDeviceModel.cs new file mode 100644 index 000000000..f3d658e12 --- /dev/null +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/IDeviceModel.cs @@ -0,0 +1,42 @@ +// 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 +{ + using System; + using System.ComponentModel.DataAnnotations; + + public interface IDeviceModel + { + /// + /// The device model identifier. + /// + public string ModelId { get; set; } + + /// + /// The device model image Url. + /// + public Uri ImageUrl { get; set; } + + /// + /// The device model name. + /// + [Required(ErrorMessage = "The device model name is required.")] + public string Name { get; set; } + + /// + /// The device model description. + /// + public string Description { get; set; } + + /// + /// A value indicating whether this instance is builtin. + /// + public bool IsBuiltin { get; set; } + + /// + /// A value indicating whether the device model supports LoRa features. + /// + public bool SupportLoRaFeatures { get; } + } +} diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceBase.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceBase.cs new file mode 100644 index 000000000..bfe802ac7 --- /dev/null +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceBase.cs @@ -0,0 +1,116 @@ +// 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.Models.v10.LoRaWAN +{ + using System; + using System.ComponentModel; + using System.ComponentModel.DataAnnotations; + using Newtonsoft.Json; + + public abstract class LoRaDeviceBase + { + /// + /// The LoRa device class. + /// Default is A. + /// + [DefaultValue(ClassType.A)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public ClassType ClassType { get; set; } + + /// + /// Allows setting the device preferred receive window (RX1 or RX2). + /// The default preferred receive window is 1. + /// + [DefaultValue(1)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int PreferredWindow { get; set; } + + /// + /// Allows controlling the handling of duplicate messages received by multiple gateways. + /// The default is Drop. + /// + [DefaultValue(DeduplicationMode.Drop)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public DeduplicationMode Deduplication { get; set; } + + /// + /// Allows setting an offset between received Datarate and retransmit datarate as specified in the LoRa Specifiations. + /// Valid for OTAA devices. + /// If an invalid value is provided the network server will use default value 0. + /// + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int? RX1DROffset { get; set; } + + /// + /// Allows setting a custom Datarate for second receive windows. + /// Valid for OTAA devices. + /// If an invalid value is provided the network server will use default value 0 (DR0). + /// + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int? RX2DataRate { get; set; } + + /// + /// Allows setting a custom wait time between receiving and transmission as specified in the specification. + /// + public int? RXDelay { get; set; } + + /// + /// Allows to disable the relax mode when using ABP. + /// By default relaxed mode is enabled. + /// + [DefaultValue(true)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool? ABPRelaxMode { get; set; } + + /// + /// Allows to explicitly specify a frame counter up start value. + /// If the device joins, this value will be used to validate the first frame and initialize the server state for the device. + /// Default is 0. + /// + [Range(0, 4294967295)] + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int? FCntUpStart { get; set; } + + /// + /// Allows to explicitly specify a frame counter down start value. + /// Default is 0. + /// + [Range(0, 4294967295)] + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int? FCntDownStart { get; set; } + + /// + /// Allows to reset the frame counters to the FCntUpStart/FCntDownStart values respectively. + /// Default is 0. + /// + [Range(0, 4294967295)] + [DefaultValue(0)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int? FCntResetCounter { get; set; } + + /// + /// Allow the usage of 32bit counters on your device. + /// + [DefaultValue(true)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool? Supports32BitFCnt { get; set; } + + /// + /// Allows defining a sliding expiration to the connection between the leaf device and IoT/Edge Hub. + /// The default is none, which causes the connection to not be dropped. + /// + [DefaultValue(null)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public int? KeepAliveTimeout { get; set; } + + /// + /// The sensor decoder API Url. + /// + public string SensorDecoder { get; set; } + } +} diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs index 261eb758c..a77f114aa 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs @@ -3,22 +3,62 @@ namespace AzureIoTHub.Portal.Models.v10.LoRaWAN { + using System; + using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; + using AzureIoTHub.Portal.Shared.Models; using Newtonsoft.Json; /// /// LoRa WAN Device details. /// - public class LoRaDeviceDetails : DeviceDetails + public class LoRaDeviceDetails : LoRaDeviceBase, IDeviceDetails { + /// + /// The name of the device. + /// + [Required(ErrorMessage = "The device should have a name.")] + public string DeviceName { get; set; } + + /// + /// The model identifier. + /// + [Required(ErrorMessage = "The device should use a model.")] + public string ModelId { get; set; } + + /// + /// The device model image Url. + /// + public Uri ImageUrl { get; set; } + + /// + /// true if this instance is connected; otherwise, false. + /// + public bool IsConnected { get; set; } + + /// + /// true if this instance is enabled; otherwise, false. + /// + public bool IsEnabled { get; set; } + + /// + /// The status updated time. + /// + public DateTime StatusUpdatedTime { get; set; } + + /// + /// List of custom device tags and their values. + /// + public Dictionary Tags { get; set; } = new(); + /// /// The device identifier. /// [Required(ErrorMessage = "The device should have a unique identifier.")] [MaxLength(ErrorMessage = "The device identifier should be up to 128 characters long.")] [RegularExpression("^[A-Z0-9]{16}$", ErrorMessage = "The device identifier must contain 16 hexadecimal characters (numbers from 0 to 9 and/or letters from A to F)")] - public override string DeviceID { get; set; } + public string DeviceID { get; set; } /// /// A value indicating whether the device uses OTAA to authenticate to LoRaWAN Network, otherwise ABP @@ -27,13 +67,6 @@ public class LoRaDeviceDetails : DeviceDetails [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] public bool UseOTAA { get; set; } - /// - /// The LoRa device class type. (default A) - /// - [DefaultValue(ClassType.A)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public ClassType ClassType { get; set; } - /// /// The OTAA App Key. /// @@ -101,11 +134,6 @@ public class LoRaDeviceDetails : DeviceDetails /// public string ReportedRXDelay { get; set; } - /// - /// The sensor decoder API Url. - /// - public string SensorDecoder { get; set; } - /// /// The GatewayID of the device. /// @@ -119,98 +147,11 @@ public class LoRaDeviceDetails : DeviceDetails public bool? Downlink { get; set; } /// - /// Allows setting the device preferred receive window (RX1 or RX2). - /// The default preferred receive window is 1. - /// - [DefaultValue(1)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? PreferredWindow { get; set; } - - /// - /// Allows controlling the handling of duplicate messages received by multiple gateways. - /// The default is Drop. - /// - [DefaultValue(DeduplicationMode.Drop)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public DeduplicationMode Deduplication { get; set; } - - /// - /// Allows setting an offset between received Datarate and retransmit datarate as specified in the LoRa Specifiations. - /// Valid for OTAA devices. - /// If an invalid value is provided the network server will use default value 0. - /// - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? RX1DROffset { get; set; } - - /// - /// Allows setting a custom Datarate for second receive windows. - /// Valid for OTAA devices. - /// If an invalid value is provided the network server will use default value 0 (DR0). - /// - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? RX2DataRate { get; set; } - - /// - /// Allows setting a custom wait time between receiving and transmission as specified in the specification. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? RXDelay { get; set; } - - /// - /// Allows to disable the relax mode when using ABP. - /// By default relaxed mode is enabled. - /// - [DefaultValue(true)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool? ABPRelaxMode { get; set; } - - /// - /// Allows to explicitly specify a frame counter up start value. - /// If the device joins, this value will be used to validate the first frame and initialize the server state for the device. - /// Default is 0. - /// - [Range(0, 4294967295)] - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? FCntUpStart { get; set; } - - /// - /// Allows to explicitly specify a frame counter down start value. - /// Default is 0. - /// - [Range(0, 4294967295)] - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? FCntDownStart { get; set; } - - /// - /// Allow the usage of 32bit counters on your device. - /// Default is true. + /// A value indicating whether the device supports LoRaWAN features. /// [DefaultValue(true)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool? Supports32BitFCnt { get; set; } - - /// - /// Allows to reset the frame counters to the FCntUpStart/FCntDownStart values respectively. - /// Default is 0. - /// - [Range(0, 4294967295)] - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? FCntResetCounter { get; set; } - - /// - /// Allows defining a sliding expiration to the connection between the leaf device and IoT/Edge Hub. - /// The default is none, which causes the connection to not be dropped. - /// - [DefaultValue(null)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public int? KeepAliveTimeout { get; set; } - - public override bool IsLoraWan => true; + public static bool IsLoraWan => true; public LoRaDeviceDetails() { diff --git a/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceModel.cs b/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceModel.cs index 2bb84a421..90defaa0a 100644 --- a/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceModel.cs +++ b/src/AzureIoTHub.Portal/Shared/Models/v1.0/LoRaWAN/LoRaDeviceModel.cs @@ -6,138 +6,66 @@ namespace AzureIoTHub.Portal.Models.v10.LoRaWAN using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; + using AzureIoTHub.Portal.Shared.Models; using Newtonsoft.Json; /// /// LoRa Device model. /// - public class LoRaDeviceModel : DeviceModel + public class LoRaDeviceModel : LoRaDeviceBase, IDeviceModel { /// - /// The LoRa device class. - /// Default is A. + /// The device model identifier. /// - [DefaultValue(ClassType.A)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public ClassType ClassType { get; set; } - - /// - /// A value indicating whether the device uses OTAA to authenticate to LoRaWAN network. Otherwise ABP. - /// Default is true. - /// - [DefaultValue(true)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool UseOTAA { get; set; } - - /// - /// The sensor decoder API Url. - /// - public string SensorDecoder { get; set; } + public string ModelId { get; set; } /// - /// Allows disabling the downstream (cloud to device) for a device. - /// By default downstream messages are enabled. - /// - [DefaultValue(true)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool? Downlink { get; set; } - - /// - /// Allows setting the device preferred receive window (RX1 or RX2). - /// The default preferred receive window is 1. + /// The device model image Url. /// - [DefaultValue(1)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int PreferredWindow { get; set; } + public Uri ImageUrl { get; set; } /// - /// Allows controlling the handling of duplicate messages received by multiple gateways. - /// The default is Drop. + /// The device model name. /// - [DefaultValue(DeduplicationMode.Drop)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public DeduplicationMode Deduplication { get; set; } + [Required(ErrorMessage = "The device model name is required.")] + public string Name { get; set; } /// - /// Allows setting an offset between received Datarate and retransmit datarate as specified in the LoRa Specifiations. - /// Valid for OTAA devices. - /// If an invalid value is provided the network server will use default value 0. + /// The device model description. /// - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? RX1DROffset { get; set; } + public string Description { get; set; } /// - /// Allows setting a custom Datarate for second receive windows. - /// Valid for OTAA devices. - /// If an invalid value is provided the network server will use default value 0 (DR0). + /// A value indicating whether this instance is builtin. /// - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? RX2DataRate { get; set; } + public bool IsBuiltin { get; set; } /// - /// Allows setting a custom wait time between receiving and transmission as specified in the specification. + /// A value indicating whether the LoRa features is supported on this model. /// - public int? RXDelay { get; set; } + public bool SupportLoRaFeatures { get; } = true; /// - /// Allows to disable the relax mode when using ABP. - /// By default relaxed mode is enabled. + /// A value indicating whether the device uses OTAA to authenticate to LoRaWAN network. Otherwise ABP. + /// Default is true. /// [DefaultValue(true)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool? ABPRelaxMode { get; set; } - - /// - /// Allows to explicitly specify a frame counter up start value. - /// If the device joins, this value will be used to validate the first frame and initialize the server state for the device. - /// Default is 0. - /// - [Range(0, 4294967295)] - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? FCntUpStart { get; set; } - - /// - /// Allows to explicitly specify a frame counter down start value. - /// Default is 0. - /// - [Range(0, 4294967295)] - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? FCntDownStart { get; set; } - - /// - /// Allows to reset the frame counters to the FCntUpStart/FCntDownStart values respectively. - /// Default is 0. - /// - [Range(0, 4294967295)] - [DefaultValue(0)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? FCntResetCounter { get; set; } + public bool UseOTAA { get; set; } /// - /// Allow the usage of 32bit counters on your device. + /// Allows disabling the downstream (cloud to device) for a device. + /// By default downstream messages are enabled. /// [DefaultValue(true)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool? Supports32BitFCnt { get; set; } - - /// - /// Allows defining a sliding expiration to the connection between the leaf device and IoT/Edge Hub. - /// The default is none, which causes the connection to not be dropped. - /// - [DefaultValue(null)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public int? KeepAliveTimeout { get; set; } - + public bool? Downlink { get; set; } /// /// Initializes a new instance of the class. /// /// The device model taht the LoRa Device model should herit. - public LoRaDeviceModel(DeviceModel from) + public LoRaDeviceModel(IDeviceModel from) { ArgumentNullException.ThrowIfNull(from, nameof(from));