diff --git a/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs b/src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs
index efc729b54..044d1d558 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 SyncDevicesJobRefreshIntervalKey = "Job:SyncDeviceJobRefreshIntervalInSeconds";
+
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..2c671a617 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 SyncDevicesJobRefreshIntervalInSeconds => this.config.GetValue(SyncDevicesJobRefreshIntervalKey, 300);
+
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 91c84a2e3..ab7c30d91 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")
@@ -184,6 +218,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..04235edd2 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 SyncDevicesJobRefreshIntervalInSeconds => this.config.GetValue(SyncDevicesJobRefreshIntervalKey, 300);
+
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..7886fbce6 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 SyncDevicesJobRefreshIntervalInSecondsConfigMustHaveDefaultValue()
+ {
+ // Arrange
+ var developmentConfigHandler = new DevelopmentConfigHandler(new ConfigurationManager());
+
+ // Assert
+ _ = developmentConfigHandler.SyncDevicesJobRefreshIntervalInSeconds.Should().Be(300);
+ }
+
[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..c52d8884d 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 SyncDevicesJobRefreshIntervalInSecondsConfigMustHaveDefaultValue()
+ {
+ // Arrange
+ var productionConfigHandler = new ProductionConfigHandler(new ConfigurationManager());
+
+ // Assert
+ _ = productionConfigHandler.SyncDevicesJobRefreshIntervalInSeconds.Should().Be(300);
+ }
+
[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/SyncDeviceJobTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTest.cs
new file mode 100644
index 000000000..4874e26b5
--- /dev/null
+++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Jobs/SyncDeviceJobTest.cs
@@ -0,0 +1,113 @@
+// 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;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using AutoMapper;
+ using AzureIoTHub.Portal.Domain;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Domain.Repositories;
+ using AzureIoTHub.Portal.Server.Jobs;
+ using AzureIoTHub.Portal.Server.Services;
+ using Microsoft.Azure.Devices.Shared;
+ using Microsoft.Extensions.Logging;
+ using Moq;
+ using NUnit.Framework;
+ using Quartz;
+
+ public class SyncDeviceJobTest : IDisposable
+ {
+ private MockRepository mockRepository;
+ private SyncDevicesJob syncDevicesJob;
+
+ private Mock> mockLogger;
+ private Mock mockDeviceService;
+ private Mock mockDeviceModelRepository;
+ private Mock mockLorawanDeviceRepository;
+ private Mock mockDeviceRepository;
+ private Mock mockUnitOfWork;
+ private Mock mockMapper;
+
+ [SetUp]
+ public void SetUp()
+ {
+ this.mockRepository = new MockRepository(MockBehavior.Strict);
+
+ this.mockLogger = this.mockRepository.Create>();
+ this.mockDeviceService = this.mockRepository.Create();
+ this.mockDeviceModelRepository = this.mockRepository.Create();
+ this.mockLorawanDeviceRepository = this.mockRepository.Create();
+ this.mockDeviceRepository = this.mockRepository.Create();
+ this.mockUnitOfWork = this.mockRepository.Create();
+ this.mockMapper = this.mockRepository.Create();
+
+ this.syncDevicesJob =
+ new SyncDevicesJob(
+ this.mockDeviceService.Object,
+ this.mockDeviceModelRepository.Object,
+ this.mockLorawanDeviceRepository.Object,
+ this.mockDeviceRepository.Object,
+ this.mockMapper.Object,
+ this.mockUnitOfWork.Object,
+ this.mockLogger.Object);
+ }
+
+ [Test]
+ public async Task ExecuteShould()
+ {
+ // Arrange
+ var mockJobExecutionContext = this.mockRepository.Create();
+ var twinCollection = new TwinCollection();
+
+ _ = 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 = Enumerable.Range(0, 100).Select(x => new Twin
+ {
+ DeviceId = FormattableString.Invariant($"{x}"),
+ Tags = twinCollection
+ }),
+ TotalItems = 1000,
+ NextPage = Guid.NewGuid().ToString()
+ });
+
+
+ _ = this.mockDeviceModelRepository
+ .Setup(x => x.GetByIdAsync(It.IsAny()))
+ .ReturnsAsync(new Portal.Domain.Entities.DeviceModel()
+ {
+ Id = Guid.NewGuid().ToString(),
+ SupportLoRaFeatures = true
+ });
+
+ // Act
+ await this.syncDevicesJob.Execute(mockJobExecutionContext.Object);
+
+ // Assert
+ this.mockRepository.VerifyAll();
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs b/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs
index 0948fa51c..7f44d9343 100644
--- a/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs
+++ b/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BackendUnitTest.cs
@@ -53,6 +53,7 @@ public virtual void Setup()
mc.AddProfile(new DeviceTagProfile());
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..f0dbad0c4
--- /dev/null
+++ b/src/AzureIoTHub.Portal/Server/Jobs/SyncDevicesJob.cs
@@ -0,0 +1,126 @@
+// 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.Collections.Generic;
+ using System.Threading.Tasks;
+ using AutoMapper;
+ using AzureIoTHub.Portal.Domain;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.Domain.Repositories;
+ using AzureIoTHub.Portal.Server.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;
+
+ 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)
+ {
+ var twins = await GetAllDeviceTwin();
+
+ foreach (var twin in twins)
+ {
+ var model = await this.deviceModelRepository.GetByIdAsync(twin.Tags["modelId"]?.ToString());
+
+ if (model == null)
+ {
+ continue;
+ }
+
+ if (model.SupportLoRaFeatures)
+ {
+ try
+ {
+ var device = this.mapper.Map(twin);
+
+ var lorawanDeviceEntity = await this.lorawanDeviceRepository.GetByIdAsync(device.Id);
+ if (lorawanDeviceEntity == null)
+ {
+ await this.lorawanDeviceRepository.InsertAsync(device);
+ }
+ else
+ {
+ if (lorawanDeviceEntity.Version < device.Version)
+ {
+ _ = this.mapper.Map(device, lorawanDeviceEntity);
+ this.lorawanDeviceRepository.Update(lorawanDeviceEntity);
+ }
+ }
+ }
+ catch (System.Exception)
+ {
+ this.logger.LogWarning($"Error while attenpting to insert LoRa device {twin.DeviceId}.");
+ }
+ }
+ else
+ {
+ 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)
+ {
+ _ = this.mapper.Map(device, deviceEntity);
+ this.deviceRepository.Update(deviceEntity);
+ }
+ }
+ }
+ }
+
+ // save entity
+ await this.unitOfWork.SaveAsync();
+ }
+
+ private async Task> GetAllDeviceTwin()
+ {
+ 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;
+ }
+ }
+}
diff --git a/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
new file mode 100644
index 000000000..b470a47c1
--- /dev/null
+++ b/src/AzureIoTHub.Portal/Server/Mappers/DeviceProfile.cs
@@ -0,0 +1,102 @@
+// 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 AutoMapper;
+ using AzureIoTHub.Portal.Domain.Entities;
+ using AzureIoTHub.Portal.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)
+ {
+ var customTags = new Dictionary();
+
+ if (twin.Tags != null)
+ {
+ var tagList = System.Text.Json.JsonSerializer.Deserialize>(twin.Tags.ToJson());
+
+ foreach (var tag in tagList)
+ {
+ if (tag.Key is not "modelId" and not "deviceName")
+ {
+ customTags.Add(tag.Key, tag.Value.ToString());
+ }
+ }
+ }
+
+ return customTags;
+ }
+
+ 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 f36d0eede..1ae070467 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;
@@ -150,6 +151,8 @@ public void ConfigureServices(IServiceCollection services)
_ = services.AddScoped();
_ = services.AddScoped();
_ = services.AddScoped();
+ _ = services.AddScoped();
+ _ = services.AddScoped();
_ = services.AddScoped();
_ = services.AddMudServices();
@@ -275,6 +278,7 @@ Specify the authorization token got from your IDP as a header.
mc.AddProfile(new DeviceTagProfile());
mc.AddProfile(new DeviceModelProfile());
mc.AddProfile(new DeviceModelCommandProfile());
+ mc.AddProfile(new DeviceProfile());
});
var mapper = mapperConfig.CreateMapper();
@@ -312,6 +316,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
+ .WithIntervalInSeconds(configuration.SyncDevicesJobRefreshIntervalInSeconds)
+ .RepeatForever()));
});
diff --git a/src/AzureIoTHubPortal.Domain/ConfigHandler.cs b/src/AzureIoTHubPortal.Domain/ConfigHandler.cs
index b4cc474bc..fe87e5d04 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 SyncDevicesJobRefreshIntervalInSeconds { 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
+ {
+ }
+}