diff --git a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs index efc729b54..57f712e6e 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs @@ -39,6 +39,8 @@ internal abstract class ConfigHandlerBase : ConfigHandler internal const string MetricExporterRefreshIntervalKey = "Metrics:ExporterRefreshIntervalInSeconds"; internal const string MetricLoaderRefreshIntervalKey = "Metrics:LoaderRefreshIntervalInMinutes"; + internal const string SyncDatabaseJobRefreshIntervalKey = "Job:SyncDatabaseJobRefreshIntervalInMinutes"; + internal const string IdeasEnabledKey = "Ideas:Enabled"; internal const string IdeasUrlKey = "Ideas:Url"; internal const string IdeasAuthenticationHeaderKey = "Ideas:Authentication:Header"; diff --git a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs index 1b0b52708..2c234eb65 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs @@ -16,6 +16,8 @@ internal DevelopmentConfigHandler(IConfiguration config) public override string PortalName => this.config[PortalNameKey]; + public override int SyncDatabaseJobRefreshIntervalInMinutes => this.config.GetValue(SyncDatabaseJobRefreshIntervalKey, 5); + public override int MetricExporterRefreshIntervalInSeconds => this.config.GetValue(MetricExporterRefreshIntervalKey, 30); public override int MetricLoaderRefreshIntervalInMinutes => this.config.GetValue(MetricLoaderRefreshIntervalKey, 10); diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.Designer.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.Designer.cs new file mode 100644 index 000000000..b2db44d9c --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.Designer.cs @@ -0,0 +1,344 @@ +// +using System; +using AzureIoTHub.Portal.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AzureIoTHub.Portal.Infrastructure.Migrations +{ + [DbContext(typeof(PortalDbContext))] + [Migration("20220922124300_Add Device and LorawanDevice")] + partial class AddDeviceandLorawanDevice + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.Device", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConnected") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ABPRelaxMode") + .HasColumnType("boolean"); + + b.Property("AppEUI") + .HasColumnType("text"); + + b.Property("Deduplication") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Downlink") + .HasColumnType("boolean"); + + b.Property("IsBuiltin") + .HasColumnType("boolean"); + + b.Property("KeepAliveTimeout") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreferredWindow") + .HasColumnType("integer"); + + b.Property("RXDelay") + .HasColumnType("integer"); + + b.Property("SensorDecoder") + .HasColumnType("text"); + + b.Property("SupportLoRaFeatures") + .HasColumnType("boolean"); + + b.Property("UseOTAA") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DeviceModels"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Confirmed") + .HasColumnType("boolean"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Frame") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsBuiltin") + .HasColumnType("boolean"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelCommands"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModelProperty", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsWritable") + .HasColumnType("boolean"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PropertyType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelProperties"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceTag", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("Searchable") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DeviceTags"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModels"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("EdgeDeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModelCommands"); + }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ABPRelaxMode") + .HasColumnType("boolean"); + + b.Property("AlreadyLoggedInOnce") + .HasColumnType("boolean"); + + b.Property("AppEUI") + .HasColumnType("text"); + + b.Property("AppKey") + .HasColumnType("text"); + + b.Property("AppSKey") + .HasColumnType("text"); + + b.Property("ClassType") + .HasColumnType("integer"); + + b.Property("DataRate") + .HasColumnType("text"); + + b.Property("Deduplication") + .HasColumnType("integer"); + + b.Property("DevAddr") + .HasColumnType("text"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downlink") + .HasColumnType("boolean"); + + b.Property("FCntDownStart") + .HasColumnType("integer"); + + b.Property("FCntResetCounter") + .HasColumnType("integer"); + + b.Property("FCntUpStart") + .HasColumnType("integer"); + + b.Property("GatewayID") + .HasColumnType("text"); + + b.Property("IsConnected") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("KeepAliveTimeout") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NbRep") + .HasColumnType("text"); + + b.Property("NwkSKey") + .HasColumnType("text"); + + b.Property("PreferredWindow") + .HasColumnType("integer"); + + b.Property("RX1DROffset") + .HasColumnType("integer"); + + b.Property("RX2DataRate") + .HasColumnType("integer"); + + b.Property("RXDelay") + .HasColumnType("integer"); + + b.Property("ReportedRX1DROffset") + .HasColumnType("text"); + + b.Property("ReportedRX2DataRate") + .HasColumnType("text"); + + b.Property("ReportedRXDelay") + .HasColumnType("text"); + + b.Property("SensorDecoder") + .HasColumnType("text"); + + b.Property("StatusUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Supports32BitFCnt") + .HasColumnType("boolean"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text"); + + b.Property("TxPower") + .HasColumnType("text"); + + b.Property("UseOTAA") + .HasColumnType("boolean"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("LorawanDevices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.cs new file mode 100644 index 000000000..b12375418 --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/20220922124300_Add Device and LorawanDevice.cs @@ -0,0 +1,89 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable + +namespace AzureIoTHub.Portal.Infrastructure.Migrations +{ + using System; + using Microsoft.EntityFrameworkCore.Migrations; + + public partial class AddDeviceandLorawanDevice : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.CreateTable( + name: "Devices", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + DeviceModelId = table.Column(type: "text", nullable: false), + IsConnected = table.Column(type: "boolean", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + Version = table.Column(type: "integer", nullable: false), + StatusUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false), + Tags = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + _ = table.PrimaryKey("PK_Devices", x => x.Id); + }); + + _ = migrationBuilder.CreateTable( + name: "LorawanDevices", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + DeviceModelId = table.Column(type: "text", nullable: false), + IsConnected = table.Column(type: "boolean", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + Version = table.Column(type: "integer", nullable: false), + StatusUpdatedTime = table.Column(type: "timestamp with time zone", nullable: false), + Tags = table.Column(type: "text", nullable: false), + UseOTAA = table.Column(type: "boolean", nullable: false), + AppKey = table.Column(type: "text", nullable: true), + AppEUI = table.Column(type: "text", nullable: true), + AppSKey = table.Column(type: "text", nullable: true), + NwkSKey = table.Column(type: "text", nullable: true), + DevAddr = table.Column(type: "text", nullable: true), + AlreadyLoggedInOnce = table.Column(type: "boolean", nullable: false), + DataRate = table.Column(type: "text", nullable: true), + TxPower = table.Column(type: "text", nullable: true), + NbRep = table.Column(type: "text", nullable: true), + ReportedRX2DataRate = table.Column(type: "text", nullable: true), + ReportedRX1DROffset = table.Column(type: "text", nullable: true), + ReportedRXDelay = table.Column(type: "text", nullable: true), + GatewayID = table.Column(type: "text", nullable: true), + Downlink = table.Column(type: "boolean", nullable: true), + ClassType = table.Column(type: "integer", nullable: false), + PreferredWindow = table.Column(type: "integer", nullable: false), + Deduplication = table.Column(type: "integer", nullable: false), + RX1DROffset = table.Column(type: "integer", nullable: true), + RX2DataRate = table.Column(type: "integer", nullable: true), + RXDelay = table.Column(type: "integer", nullable: true), + ABPRelaxMode = table.Column(type: "boolean", nullable: true), + FCntUpStart = table.Column(type: "integer", nullable: true), + FCntDownStart = table.Column(type: "integer", nullable: true), + FCntResetCounter = table.Column(type: "integer", nullable: true), + Supports32BitFCnt = table.Column(type: "boolean", nullable: true), + KeepAliveTimeout = table.Column(type: "integer", nullable: true), + SensorDecoder = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + _ = table.PrimaryKey("PK_LorawanDevices", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropTable( + name: "Devices"); + + _ = migrationBuilder.DropTable( + name: "LorawanDevices"); + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs index ee7f83f1e..b80b1060f 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/Migrations/PortalDbContextModelSnapshot.cs @@ -17,11 +17,45 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("ProductVersion", "6.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.Device", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConnected") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Devices"); + }); + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.DeviceModel", b => { b.Property("Id") @@ -188,6 +222,124 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("EdgeDeviceModelCommands"); }); + + modelBuilder.Entity("AzureIoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ABPRelaxMode") + .HasColumnType("boolean"); + + b.Property("AlreadyLoggedInOnce") + .HasColumnType("boolean"); + + b.Property("AppEUI") + .HasColumnType("text"); + + b.Property("AppKey") + .HasColumnType("text"); + + b.Property("AppSKey") + .HasColumnType("text"); + + b.Property("ClassType") + .HasColumnType("integer"); + + b.Property("DataRate") + .HasColumnType("text"); + + b.Property("Deduplication") + .HasColumnType("integer"); + + b.Property("DevAddr") + .HasColumnType("text"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downlink") + .HasColumnType("boolean"); + + b.Property("FCntDownStart") + .HasColumnType("integer"); + + b.Property("FCntResetCounter") + .HasColumnType("integer"); + + b.Property("FCntUpStart") + .HasColumnType("integer"); + + b.Property("GatewayID") + .HasColumnType("text"); + + b.Property("IsConnected") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("KeepAliveTimeout") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NbRep") + .HasColumnType("text"); + + b.Property("NwkSKey") + .HasColumnType("text"); + + b.Property("PreferredWindow") + .HasColumnType("integer"); + + b.Property("RX1DROffset") + .HasColumnType("integer"); + + b.Property("RX2DataRate") + .HasColumnType("integer"); + + b.Property("RXDelay") + .HasColumnType("integer"); + + b.Property("ReportedRX1DROffset") + .HasColumnType("text"); + + b.Property("ReportedRX2DataRate") + .HasColumnType("text"); + + b.Property("ReportedRXDelay") + .HasColumnType("text"); + + b.Property("SensorDecoder") + .HasColumnType("text"); + + b.Property("StatusUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Supports32BitFCnt") + .HasColumnType("boolean"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text"); + + b.Property("TxPower") + .HasColumnType("text"); + + b.Property("UseOTAA") + .HasColumnType("boolean"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("LorawanDevices"); + }); #pragma warning restore 612, 618 } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs index adb8bca34..1eeefbcb6 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/PortalDbContext.cs @@ -9,6 +9,7 @@ namespace AzureIoTHub.Portal.Infrastructure using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; public class PortalDbContext : DbContext { @@ -16,6 +17,8 @@ public class PortalDbContext : DbContext public DbSet DeviceTags { get; set; } public DbSet DeviceModelCommands { get; set; } public DbSet DeviceModels { get; set; } + public DbSet Devices { get; set; } + public DbSet LorawanDevices { get; set; } public DbSet EdgeDeviceModels { get; set; } public DbSet EdgeDeviceModelCommands { get; set; } @@ -25,5 +28,22 @@ public PortalDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + _ = modelBuilder.Entity() + .Property(b => b.Tags) + .HasConversion( + v => JsonConvert.SerializeObject(v), + v => JsonConvert.DeserializeObject>(v)); + + _ = modelBuilder.Entity() + .Property(b => b.Tags) + .HasConversion( + v => JsonConvert.SerializeObject(v), + v => JsonConvert.DeserializeObject>(v)); + } } } diff --git a/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs b/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs index b2e423dd2..e8195773f 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs +++ b/src/AzureIoTHub.Portal.Infrastructure/ProductionConfigHandler.cs @@ -16,6 +16,8 @@ internal ProductionConfigHandler(IConfiguration config) public override string PortalName => this.config[PortalNameKey]; + public override int SyncDatabaseJobRefreshIntervalInMinutes => this.config.GetValue(SyncDatabaseJobRefreshIntervalKey, 5); + public override int MetricExporterRefreshIntervalInSeconds => this.config.GetValue(MetricExporterRefreshIntervalKey, 30); public override int MetricLoaderRefreshIntervalInMinutes => this.config.GetValue(MetricLoaderRefreshIntervalKey, 10); diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs new file mode 100644 index 000000000..ae814479a --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/DeviceRepository.cs @@ -0,0 +1,15 @@ +// 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.Infrastructure.Repositories +{ + using AzureIoTHub.Portal.Domain.Repositories; + using Domain.Entities; + + public class DeviceRepository : GenericRepository, IDeviceRepository + { + public DeviceRepository(PortalDbContext context) : base(context) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs b/src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs new file mode 100644 index 000000000..1ab71dd99 --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Repositories/LorawanDeviceRepository.cs @@ -0,0 +1,15 @@ +// 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.Infrastructure.Repositories +{ + using AzureIoTHub.Portal.Domain.Repositories; + using Domain.Entities; + + public class LorawanDeviceRepository : GenericRepository, ILorawanDeviceRepository + { + public LorawanDeviceRepository(PortalDbContext context) : base(context) + { + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs index 7d389c50b..40a5773f4 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs @@ -159,6 +159,16 @@ public void SettingsShouldGetBoolFromAppSettings(string configKey, string config this.mockRepository.VerifyAll(); } + [Test] + public void SyncDatabaseJobRefreshIntervalInMinutesConfigMustHaveDefaultValue() + { + // Arrange + var developmentConfigHandler = new DevelopmentConfigHandler(new ConfigurationManager()); + + // Assert + _ = developmentConfigHandler.SyncDatabaseJobRefreshIntervalInMinutes.Should().Be(5); + } + [Test] public void MetricExporterRefreshIntervalInSecondsConfigMustHaveDefaultValue() { diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs index 5b9c09e1f..0a024780b 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/ProductionConfigHandlerTests.cs @@ -3,10 +3,8 @@ namespace AzureIoTHub.Portal.Tests.Unit.Infrastructure { - using AzureIoTHub.Portal.Server; using System; using System.Globalization; - using System.Reflection; using FluentAssertions; using Microsoft.Extensions.Configuration; using Moq; @@ -185,6 +183,16 @@ public void SettingsShouldGetBoolFromAppSettings(string configKey, string config this.mockRepository.VerifyAll(); } + [Test] + public void SyncDatabaseJobRefreshIntervalInMinutesConfigMustHaveDefaultValue() + { + // Arrange + var productionConfigHandler = new ProductionConfigHandler(new ConfigurationManager()); + + // Assert + _ = productionConfigHandler.SyncDatabaseJobRefreshIntervalInMinutes.Should().Be(5); + } + [Test] public void MetricExporterRefreshIntervalInSecondsConfigMustHaveDefaultValue() { diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs new file mode 100644 index 000000000..412dfff07 --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/DeviceRepositoryTest.cs @@ -0,0 +1,43 @@ +// 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.Tests.Unit.Infrastructure.Repositories +{ + using System.Linq; + using System.Threading.Tasks; + using AutoFixture; + using AzureIoTHub.Portal.Domain.Entities; + using AzureIoTHub.Portal.Infrastructure.Repositories; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using FluentAssertions; + using NUnit.Framework; + + public class DeviceRepositoryTest : BackendUnitTest + { + private DeviceRepository deviceRepository; + + public override void Setup() + { + base.Setup(); + + this.deviceRepository = new DeviceRepository(DbContext); + } + + [Test] + public async Task GetAllShouldReturnExpectedDevices() + { + // Arrange + var expectedDevices = Fixture.CreateMany(5).ToList(); + + await DbContext.AddRangeAsync(expectedDevices); + + _ = await DbContext.SaveChangesAsync(); + + //Act + var result = this.deviceRepository.GetAll().ToList(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDevices); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs new file mode 100644 index 000000000..71aaecdae --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Infrastructure/Repositories/LorawanDeviceRepositoryTest.cs @@ -0,0 +1,43 @@ +// 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.Tests.Unit.Infrastructure.Repositories +{ + using AutoFixture; + using System.Threading.Tasks; + using AzureIoTHub.Portal.Infrastructure.Repositories; + using AzureIoTHub.Portal.Tests.Unit.UnitTests.Bases; + using NUnit.Framework; + using AzureIoTHub.Portal.Domain.Entities; + using System.Linq; + using FluentAssertions; + + public class LorawanDeviceRepositoryTest : BackendUnitTest + { + private LorawanDeviceRepository lorawanDeviceRepository; + + public override void Setup() + { + base.Setup(); + + this.lorawanDeviceRepository = new LorawanDeviceRepository(DbContext); + } + + [Test] + public async Task GetAllShouldReturnExpectedDevices() + { + // Arrange + var expectedDevices = Fixture.CreateMany(5).ToList(); + + await DbContext.AddRangeAsync(expectedDevices); + + _ = await DbContext.SaveChangesAsync(); + + //Act + var result = this.lorawanDeviceRepository.GetAll().ToList(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDevices); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTests.cs new file mode 100644 index 000000000..d3b43a534 --- /dev/null +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTests.cs @@ -0,0 +1,304 @@ +// 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.Tests.Unit.Server.Jobs +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using AutoFixture; + using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Domain.Repositories; + using AzureIoTHub.Portal.Server.Jobs; + using AzureIoTHub.Portal.Server.Services; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.DependencyInjection; + using Moq; + using NUnit.Framework; + using Portal.Domain.Entities; + using Quartz; + using UnitTests.Bases; + + public class SyncDeviceJobTests : BackendUnitTest + { + private IJob syncDevicesJob; + + private Mock mockDeviceService; + private Mock mockDeviceModelRepository; + private Mock mockLorawanDeviceRepository; + private Mock mockDeviceRepository; + private Mock mockUnitOfWork; + + public override void Setup() + { + base.Setup(); + + this.mockDeviceService = MockRepository.Create(); + this.mockDeviceModelRepository = MockRepository.Create(); + this.mockLorawanDeviceRepository = MockRepository.Create(); + this.mockDeviceRepository = MockRepository.Create(); + this.mockUnitOfWork = MockRepository.Create(); + + + _ = ServiceCollection.AddSingleton(this.mockDeviceService.Object); + _ = ServiceCollection.AddSingleton(this.mockDeviceModelRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockLorawanDeviceRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockDeviceRepository.Object); + _ = ServiceCollection.AddSingleton(this.mockUnitOfWork.Object); + _ = ServiceCollection.AddSingleton(); + + Services = ServiceCollection.BuildServiceProvider(); + + this.syncDevicesJob = Services.GetRequiredService(); + } + + [Test] + public async Task Execute_NewDevice_DeviceCreated() + { + // Arrange + var mockJobExecutionContext = MockRepository.Create(); + + var expectedDeviceModel = Fixture.Create(); + expectedDeviceModel.SupportLoRaFeatures = false; + + var expectedTwinDevice = new Twin + { + DeviceId = Fixture.Create(), + Tags = new TwinCollection + { + ["modelId"] = expectedDeviceModel.Id, + ["deviceName"] = Fixture.Create() + } + }; + + _ = this.mockDeviceService + .Setup(x => x.GetAllDevice( + It.IsAny(), + It.IsAny(), + It.Is(x => x == "LoRa Concentrator"), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.Is(x => x == 100))) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + expectedTwinDevice + }, + TotalItems = 1 + }); + + + _ = this.mockDeviceModelRepository + .Setup(x => x.GetByIdAsync(expectedDeviceModel.Id)) + .ReturnsAsync(expectedDeviceModel); + + _ = this.mockDeviceRepository.Setup(repository => repository.GetByIdAsync(expectedTwinDevice.DeviceId)) + .ReturnsAsync((Device)null); + + _ = this.mockDeviceRepository.Setup(repository => repository.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + // Act + await this.syncDevicesJob.Execute(mockJobExecutionContext.Object); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task Execute_ExistingDeviceWithGreeterVersion_DeviceUpdated() + { + // Arrange + var mockJobExecutionContext = MockRepository.Create(); + + var expectedDeviceModel = Fixture.Create(); + expectedDeviceModel.SupportLoRaFeatures = false; + + var expectedTwinDevice = new Twin + { + DeviceId = Fixture.Create(), + Tags = new TwinCollection + { + ["modelId"] = expectedDeviceModel.Id, + ["deviceName"] = Fixture.Create() + }, + Version = 2 + }; + + var existingDevice = new Device + { + Id = expectedTwinDevice.DeviceId, + Version = 1 + }; + + _ = this.mockDeviceService + .Setup(x => x.GetAllDevice( + It.IsAny(), + It.IsAny(), + It.Is(x => x == "LoRa Concentrator"), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.Is(x => x == 100))) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + expectedTwinDevice + }, + TotalItems = 1 + }); + + + _ = this.mockDeviceModelRepository + .Setup(x => x.GetByIdAsync(expectedDeviceModel.Id)) + .ReturnsAsync(expectedDeviceModel); + + _ = this.mockDeviceRepository.Setup(repository => repository.GetByIdAsync(expectedTwinDevice.DeviceId)) + .ReturnsAsync(existingDevice); + + this.mockDeviceRepository.Setup(repository => repository.Update(It.IsAny())) + .Verifiable(); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + // Act + await this.syncDevicesJob.Execute(mockJobExecutionContext.Object); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task Execute_NewLorawanDevice_LorawanDeviceCreated() + { + // Arrange + var mockJobExecutionContext = MockRepository.Create(); + + var expectedDeviceModel = Fixture.Create(); + expectedDeviceModel.SupportLoRaFeatures = true; + + var expectedTwinDevice = new Twin + { + DeviceId = Fixture.Create(), + Tags = new TwinCollection + { + ["modelId"] = expectedDeviceModel.Id, + ["deviceName"] = Fixture.Create() + } + }; + + _ = this.mockDeviceService + .Setup(x => x.GetAllDevice( + It.IsAny(), + It.IsAny(), + It.Is(x => x == "LoRa Concentrator"), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.Is(x => x == 100))) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + expectedTwinDevice + }, + TotalItems = 1 + }); + + + _ = this.mockDeviceModelRepository + .Setup(x => x.GetByIdAsync(expectedDeviceModel.Id)) + .ReturnsAsync(expectedDeviceModel); + + _ = this.mockLorawanDeviceRepository.Setup(repository => repository.GetByIdAsync(expectedTwinDevice.DeviceId)) + .ReturnsAsync((LorawanDevice)null); + + _ = this.mockLorawanDeviceRepository.Setup(repository => repository.InsertAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + // Act + await this.syncDevicesJob.Execute(mockJobExecutionContext.Object); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task Execute_ExistingLorawanDeviceWithGreeterVersion_LorawanDeviceUpdated() + { + // Arrange + var mockJobExecutionContext = MockRepository.Create(); + + var expectedDeviceModel = Fixture.Create(); + expectedDeviceModel.SupportLoRaFeatures = true; + + var expectedTwinDevice = new Twin + { + DeviceId = Fixture.Create(), + Tags = new TwinCollection + { + ["modelId"] = expectedDeviceModel.Id, + ["deviceName"] = Fixture.Create() + }, + Version = 2 + }; + + var existingLorawanDevice = new LorawanDevice + { + Id = expectedTwinDevice.DeviceId, + Version = 1 + }; + + _ = this.mockDeviceService + .Setup(x => x.GetAllDevice( + It.IsAny(), + It.IsAny(), + It.Is(x => x == "LoRa Concentrator"), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.Is(x => x == 100))) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + expectedTwinDevice + }, + TotalItems = 1 + }); + + + _ = this.mockDeviceModelRepository + .Setup(x => x.GetByIdAsync(expectedDeviceModel.Id)) + .ReturnsAsync(expectedDeviceModel); + + _ = this.mockLorawanDeviceRepository.Setup(repository => repository.GetByIdAsync(expectedTwinDevice.DeviceId)) + .ReturnsAsync(existingLorawanDevice); + + this.mockLorawanDeviceRepository.Setup(repository => repository.Update(It.IsAny())) + .Verifiable(); + + _ = this.mockUnitOfWork.Setup(work => work.SaveAsync()) + .Returns(Task.CompletedTask); + + // Act + await this.syncDevicesJob.Execute(mockJobExecutionContext.Object); + + // Assert + MockRepository.VerifyAll(); + } + } +} diff --git a/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs index bb9667a3b..b1ede3ecc 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs @@ -55,6 +55,7 @@ public virtual void Setup() mc.AddProfile(new EdgeDeviceModelCommandProfile()); mc.AddProfile(new DeviceModelProfile()); mc.AddProfile(new DeviceModelCommandProfile()); + mc.AddProfile(new DeviceProfile()); }); Mapper = mappingConfig.CreateMapper(); _ = ServiceCollection.AddSingleton(Mapper); diff --git a/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs new file mode 100644 index 000000000..19190e745 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs @@ -0,0 +1,144 @@ +// 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.Jobs +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using AutoMapper; + using Domain; + using AzureIoTHub.Portal.Domain.Entities; + using Domain.Repositories; + using Services; + using Microsoft.Azure.Devices.Shared; + using Microsoft.Extensions.Logging; + using Quartz; + + [DisallowConcurrentExecution] + public class SyncDevicesJob : IJob + { + private readonly IDeviceService deviceService; + private readonly IDeviceModelRepository deviceModelRepository; + private readonly ILorawanDeviceRepository lorawanDeviceRepository; + private readonly IDeviceRepository deviceRepository; + private readonly IMapper mapper; + private readonly IUnitOfWork unitOfWork; + private readonly ILogger logger; + + private const string ModelId = "modelId"; + + public SyncDevicesJob(IDeviceService deviceService, + IDeviceModelRepository deviceModelRepository, + ILorawanDeviceRepository lorawanDeviceRepository, + IDeviceRepository deviceRepository, + IMapper mapper, + IUnitOfWork unitOfWork, + ILogger logger) + { + this.deviceService = deviceService; + this.deviceModelRepository = deviceModelRepository; + this.lorawanDeviceRepository = lorawanDeviceRepository; + this.deviceRepository = deviceRepository; + this.mapper = mapper; + this.unitOfWork = unitOfWork; + this.logger = logger; + } + + public async Task Execute(IJobExecutionContext context) + { + try + { + this.logger.LogInformation("Start of sync devices job"); + + await SyncDevices(); + + this.logger.LogInformation("End of sync devices job"); + } + catch (Exception e) + { + this.logger.LogError(e, "Sync devices job has failed"); + } + } + + private async Task SyncDevices() + { + foreach (var twin in await GetTwinDevices()) + { + var deviceModel = await this.deviceModelRepository.GetByIdAsync(twin.Tags[ModelId]?.ToString() ?? string.Empty); + + if (deviceModel == null) + { + this.logger.LogWarning($"The device with wont be synched, its model id {twin.Tags[ModelId]?.ToString()} doesn't exist"); + continue; + } + + if (deviceModel.SupportLoRaFeatures) + { + await CreateOrUpdateLorawanDevice(twin); + } + else + { + await CreateOrUpdateDevice(twin); + } + } + + await this.unitOfWork.SaveAsync(); + } + + private async Task> GetTwinDevices() + { + var twins = new List(); + var continuationToken = string.Empty; + + int totalTwinDevices; + do + { + var result = await this.deviceService.GetAllDevice(continuationToken: continuationToken,excludeDeviceType: "LoRa Concentrator", pageSize: 100); + twins.AddRange(result.Items); + + totalTwinDevices = result.TotalItems; + continuationToken = result.NextPage; + + } while (totalTwinDevices > twins.Count); + + return twins; + } + + private async Task CreateOrUpdateLorawanDevice(Twin twin) + { + var lorawanDevice = this.mapper.Map(twin); + + var lorawanDeviceEntity = await this.lorawanDeviceRepository.GetByIdAsync(lorawanDevice.Id); + if (lorawanDeviceEntity == null) + { + await this.lorawanDeviceRepository.InsertAsync(lorawanDevice); + } + else + { + if (lorawanDeviceEntity.Version >= lorawanDevice.Version) return; + + _ = this.mapper.Map(lorawanDevice, lorawanDeviceEntity); + this.lorawanDeviceRepository.Update(lorawanDeviceEntity); + } + } + + private async Task CreateOrUpdateDevice(Twin twin) + { + var device = this.mapper.Map(twin); + + var deviceEntity = await this.deviceRepository.GetByIdAsync(device.Id); + if (deviceEntity == null) + { + await this.deviceRepository.InsertAsync(device); + } + else + { + if (deviceEntity.Version >= device.Version) return; + + _ = this.mapper.Map(device, deviceEntity); + this.deviceRepository.Update(deviceEntity); + } + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs new file mode 100644 index 000000000..06d8dc8d2 --- /dev/null +++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs @@ -0,0 +1,92 @@ +// 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.Mappers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.Json; + using AutoMapper; + using AzureIoTHub.Portal.Domain.Entities; + using Models.v10.LoRaWAN; + using Microsoft.Azure.Devices.Shared; + + public class DeviceProfile : Profile + { + public DeviceProfile() + { + _ = CreateMap(); + _ = CreateMap() + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceId)) + .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Tags["deviceName"])) + .ForMember(dest => dest.DeviceModelId, opts => opts.MapFrom(src => src.Tags["modelId"])) + .ForMember(dest => dest.Version, opts => opts.MapFrom(src => src.Version)) + .ForMember(dest => dest.IsConnected, opts => opts.MapFrom(src => src.ConnectionState == Microsoft.Azure.Devices.DeviceConnectionState.Connected)) + .ForMember(dest => dest.IsEnabled, opts => opts.MapFrom(src => src.Status == Microsoft.Azure.Devices.DeviceStatus.Enabled)) + .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => GetTags(src))); + + + _ = CreateMap(); + _ = CreateMap() + .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceId)) + .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Tags["deviceName"])) + .ForMember(dest => dest.DeviceModelId, opts => opts.MapFrom(src => src.Tags["modelId"])) + .ForMember(dest => dest.Version, opts => opts.MapFrom(src => src.Version)) + .ForMember(dest => dest.IsConnected, opts => opts.MapFrom(src => src.ConnectionState == Microsoft.Azure.Devices.DeviceConnectionState.Connected)) + .ForMember(dest => dest.IsEnabled, opts => opts.MapFrom(src => src.Status == Microsoft.Azure.Devices.DeviceStatus.Enabled)) + .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => GetTags(src))) + .ForMember(dest => dest.UseOTAA, opts => opts.MapFrom(src => !string.IsNullOrEmpty(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppEUI))))) + .ForMember(dest => dest.AppKey, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppKey)))) + .ForMember(dest => dest.AppEUI, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppEUI)))) + .ForMember(dest => dest.AppSKey, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.AppSKey)))) + .ForMember(dest => dest.NwkSKey, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.NwkSKey)))) + .ForMember(dest => dest.DevAddr, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.DevAddr)))) + .ForMember(dest => dest.AlreadyLoggedInOnce, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, "DevAddr") != null)) + .ForMember(dest => dest.DataRate, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.DataRate)))) + .ForMember(dest => dest.TxPower, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.TxPower)))) + .ForMember(dest => dest.NbRep, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.NbRep)))) + .ForMember(dest => dest.ReportedRX2DataRate, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRX2DataRate)))) + .ForMember(dest => dest.ReportedRX1DROffset, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRX1DROffset)))) + .ForMember(dest => dest.ReportedRXDelay, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.ReportedRXDelay)))) + .ForMember(dest => dest.GatewayID, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveReportedPropertyValue(src, nameof(LoRaDeviceDetails.GatewayID)))) + .ForMember(dest => dest.Downlink, opts => opts.MapFrom(src => GetDesiredPropertyAsBooleanValue(src, nameof(LoRaDeviceDetails.Downlink)))) + .ForMember(dest => dest.RX1DROffset, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.RX1DROffset)))) + .ForMember(dest => dest.RX2DataRate, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.RX2DataRate)))) + .ForMember(dest => dest.RXDelay, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.RXDelay)))) + .ForMember(dest => dest.ABPRelaxMode, opts => opts.MapFrom(src => GetDesiredPropertyAsBooleanValue(src, nameof(LoRaDeviceDetails.ABPRelaxMode)))) + .ForMember(dest => dest.FCntUpStart, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.FCntUpStart)))) + .ForMember(dest => dest.FCntDownStart, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.FCntDownStart)))) + .ForMember(dest => dest.FCntResetCounter, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.FCntResetCounter)))) + .ForMember(dest => dest.Supports32BitFCnt, opts => opts.MapFrom(src => GetDesiredPropertyAsBooleanValue(src, nameof(LoRaDeviceDetails.Supports32BitFCnt)))) + .ForMember(dest => dest.KeepAliveTimeout, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.KeepAliveTimeout)))) + .ForMember(dest => dest.SensorDecoder, opts => opts.MapFrom(src => Helpers.DeviceHelper.RetrieveDesiredPropertyValue(src, nameof(LoRaDeviceDetails.SensorDecoder)))) + .ForMember(dest => dest.Deduplication, opts => opts.MapFrom(src => GetDesiredPropertyAsEnum(src, nameof(LoRaDeviceDetails.Deduplication)))) + .ForMember(dest => dest.PreferredWindow, opts => opts.MapFrom(src => GetDesiredPropertyAsIntegerValue(src, nameof(LoRaDeviceDetails.PreferredWindow)) ?? 0)) + .ForMember(dest => dest.ClassType, opts => opts.MapFrom(src => GetDesiredPropertyAsEnum(src, nameof(LoRaDeviceDetails.ClassType)))); + } + + private static Dictionary GetTags(Twin twin) + { + return (JsonSerializer.Deserialize>(twin.Tags.ToJson()) ?? new Dictionary()) + .Where(tag => tag.Key is not "modelId" and not "deviceName") + .Select(tag => new KeyValuePair(tag.Key, tag.Value.ToString())) + .ToDictionary(tag => tag.Key, tag => tag.Value); + } + + private static bool? GetDesiredPropertyAsBooleanValue(Twin twin, string propertyName) + { + return bool.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, propertyName), out var boolResult) ? boolResult : null; + } + + private static int? GetDesiredPropertyAsIntegerValue(Twin twin, string propertyName) + { + return int.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, propertyName), out var numberStyles) ? numberStyles : null; + } + + private static TEnum GetDesiredPropertyAsEnum(Twin twin, string propertyName) where TEnum : struct, IConvertible + { + return Enum.TryParse(Helpers.DeviceHelper.RetrieveDesiredPropertyValue(twin, propertyName), out var result) ? result : default; + } + } +} diff --git a/src/AzureIoTHub.Portal/Server/Startup.cs b/src/AzureIoTHub.Portal/Server/Startup.cs index eb4c1255f..4f7265bf4 100644 --- a/src/AzureIoTHub.Portal/Server/Startup.cs +++ b/src/AzureIoTHub.Portal/Server/Startup.cs @@ -18,6 +18,7 @@ namespace AzureIoTHub.Portal.Server using AzureIoTHub.Portal.Infrastructure.Factories; using AzureIoTHub.Portal.Infrastructure.Repositories; using AzureIoTHub.Portal.Infrastructure.Seeds; + using AzureIoTHub.Portal.Server.Jobs; using Extensions; using Hellang.Middleware.ProblemDetails; using Hellang.Middleware.ProblemDetails.Mvc; @@ -152,6 +153,8 @@ public void ConfigureServices(IServiceCollection services) _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddMudServices(); @@ -279,6 +282,7 @@ Specify the authorization token got from your IDP as a header. mc.AddProfile(new EdgeDeviceModelCommandProfile()); mc.AddProfile(new DeviceModelProfile()); mc.AddProfile(new DeviceModelCommandProfile()); + mc.AddProfile(new DeviceProfile()); }); var mapper = mapperConfig.CreateMapper(); @@ -316,6 +320,14 @@ Specify the authorization token got from your IDP as a header. q.AddMetricsService(configuration); q.AddMetricsService(configuration); q.AddMetricsService(configuration); + + _ = q.AddJob(j => j.WithIdentity(nameof(SyncDevicesJob))) + .AddTrigger(t => t + .WithIdentity($"{nameof(SyncDevicesJob)}") + .ForJob(nameof(SyncDevicesJob)) + .WithSimpleSchedule(s => s + .WithIntervalInMinutes(configuration.SyncDatabaseJobRefreshIntervalInMinutes) + .RepeatForever())); }); diff --git a/src/AzureIoTHubPortal.Domain/ConfigHandler.cs b/src/AzureIoTHubPortal.Domain/ConfigHandler.cs index b4cc474bc..1c87f5d8a 100644 --- a/src/AzureIoTHubPortal.Domain/ConfigHandler.cs +++ b/src/AzureIoTHubPortal.Domain/ConfigHandler.cs @@ -51,6 +51,8 @@ public abstract class ConfigHandler public abstract string PortalName { get; } + public abstract int SyncDatabaseJobRefreshIntervalInMinutes { get; } + public abstract int MetricExporterRefreshIntervalInSeconds { get; } public abstract int MetricLoaderRefreshIntervalInMinutes { get; } diff --git a/src/AzureIoTHubPortal.Domain/Entities/Device.cs b/src/AzureIoTHubPortal.Domain/Entities/Device.cs new file mode 100644 index 000000000..97b7ccefd --- /dev/null +++ b/src/AzureIoTHubPortal.Domain/Entities/Device.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.Domain.Entities +{ + using AzureIoTHub.Portal.Domain.Base; + + public class Device : EntityBase + { + /// + /// The name of the device. + /// + public string Name { get; set; } + + /// + /// The model identifier. + /// + public string DeviceModelId { 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; } + + public int Version { 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(); + } +} diff --git a/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs new file mode 100644 index 000000000..f1af440d5 --- /dev/null +++ b/src/AzureIoTHubPortal.Domain/Entities/LorawanDevice.cs @@ -0,0 +1,202 @@ +// 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.Domain.Entities +{ + using AzureIoTHub.Portal.Domain.Base; + using AzureIoTHub.Portal.Models.v10.LoRaWAN; + + public class LorawanDevice : EntityBase + { + + /// + /// The name of the device. + /// + public string Name { get; set; } + + /// + /// The model identifier. + /// + public string DeviceModelId { 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; } + + public int Version { 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(); + + /// + /// A value indicating whether the device uses OTAA to authenticate to LoRaWAN Network, otherwise ABP + /// + public bool UseOTAA { get; set; } + + /// + /// The OTAA App Key. + /// + public string? AppKey { get; set; } + + /// + /// The device OTAA Application EUI. + /// + public string? AppEUI { get; set; } + + /// + /// The ABP AppSKey. + /// + public string? AppSKey { get; set; } + + /// + /// The ABP NwkSKey. + /// + public string? NwkSKey { get; set; } + + /// + /// Unique identifier that allows + /// the device to be recognized. + /// + public string? DevAddr { get; set; } + + /// + /// A value indicating whether the device has already joined the platform. + /// + public bool AlreadyLoggedInOnce { get; set; } + + /// + /// The Device Current Datarate, + /// This value will be only reported if you are using Adaptive Data Rate. + /// + public string? DataRate { get; set; } + + /// + /// The Device Current Transmit Power, + /// This value will be only reported if you are using Adaptive Data Rate. + /// + public string? TxPower { get; set; } + + /// + /// The Device Current repetition when transmitting. + /// E.g. if set to two, the device will transmit twice his upstream messages. + /// This value will be only reported if you are using Adaptive Data Rate. + /// + public string? NbRep { get; set; } + + /// + /// The Device Current Rx2Datarate. + /// + public string? ReportedRX2DataRate { get; set; } + + /// + /// The Device Current RX1DROffset. + /// + public string? ReportedRX1DROffset { get; set; } + + /// + /// The Device Current RXDelay. + /// + public string? ReportedRXDelay { get; set; } + + /// + /// The GatewayID of the device. + /// + public string? GatewayID { get; set; } + + /// + /// A value indicating whether the downlinks are enabled (True if not provided) + /// + public bool? Downlink { get; set; } + + /// + /// The LoRa device class. + /// Default is A. + /// + public ClassType ClassType { get; set; } + + /// + /// Allows setting the device preferred receive window (RX1 or RX2). + /// The default preferred receive window is 1. + /// + public int PreferredWindow { get; set; } + + /// + /// Allows controlling the handling of duplicate messages received by multiple gateways. + /// The default is Drop. + /// + 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. + /// + 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). + /// + 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. + /// + 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. + /// + public int? FCntUpStart { get; set; } + + /// + /// Allows to explicitly specify a frame counter down start value. + /// Default is 0. + /// + public int? FCntDownStart { get; set; } + + /// + /// Allows to reset the frame counters to the FCntUpStart/FCntDownStart values respectively. + /// Default is 0. + /// + public int? FCntResetCounter { get; set; } + + /// + /// Allow the usage of 32bit counters on your device. + /// + 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. + /// + public int? KeepAliveTimeout { get; set; } + + /// + /// The sensor decoder API Url. + /// + public string? SensorDecoder { get; set; } + } +} diff --git a/src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.cs b/src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.cs new file mode 100644 index 000000000..8037dc49b --- /dev/null +++ b/src/AzureIoTHubPortal.Domain/Repositories/IDeviceRepository.cs @@ -0,0 +1,11 @@ +// 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.Domain.Repositories +{ + using Entities; + + public interface IDeviceRepository : IRepository + { + } +} diff --git a/src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.cs b/src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.cs new file mode 100644 index 000000000..1678577e8 --- /dev/null +++ b/src/AzureIoTHubPortal.Domain/Repositories/ILorawanDeviceRepository.cs @@ -0,0 +1,11 @@ +// 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.Domain.Repositories +{ + using Entities; + + public interface ILorawanDeviceRepository : IRepository + { + } +}