From 2d48de2d793a131267a3df2ef97c4f2eb487c657 Mon Sep 17 00:00:00 2001 From: E068097 Date: Fri, 11 Oct 2024 11:46:08 +0200 Subject: [PATCH 01/17] Add of layerId in device twin --- src/IoTHub.Portal.Application/Mappers/DeviceProfile.cs | 6 ++++++ .../Mappers/DeviceTwinMapper.cs | 7 +++++-- .../Mappers/LoRaDeviceMapper.cs | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/IoTHub.Portal.Application/Mappers/DeviceProfile.cs b/src/IoTHub.Portal.Application/Mappers/DeviceProfile.cs index 7b3a9f4de..eae9d1915 100644 --- a/src/IoTHub.Portal.Application/Mappers/DeviceProfile.cs +++ b/src/IoTHub.Portal.Application/Mappers/DeviceProfile.cs @@ -15,6 +15,7 @@ public DeviceProfile() .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceID)) .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.DeviceName)) .ForMember(dest => dest.DeviceModelId, opts => opts.MapFrom(src => src.ModelId)) + .ForMember(dest => dest.LayerId, opts => opts.MapFrom(src => src.LayerId)) .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => src.Tags.Select(pair => new DeviceTagValue { Name = pair.Key, @@ -25,12 +26,14 @@ public DeviceProfile() .ForMember(dest => dest.DeviceID, opts => opts.MapFrom(src => src.Id)) .ForMember(dest => dest.DeviceName, opts => opts.MapFrom(src => src.Name)) .ForMember(dest => dest.ModelId, opts => opts.MapFrom(src => src.DeviceModelId)) + .ForMember(dest => dest.LayerId, opts => opts.MapFrom(src => src.LayerId)) .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => src.Tags.ToDictionary(tag => tag.Name, tag => tag.Value))); _ = 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.LayerId, opts => opts.MapFrom(src => src.Tags.Contains("layerId") ? src.Tags["layerId"] : null)) .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)) @@ -42,6 +45,7 @@ public DeviceProfile() .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DeviceID)) .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.DeviceName)) .ForMember(dest => dest.DeviceModelId, opts => opts.MapFrom(src => src.ModelId)) + .ForMember(dest => dest.LayerId, opts => opts.MapFrom(src => src.LayerId)) .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => src.Tags.Select(pair => new DeviceTagValue { Name = pair.Key, @@ -52,12 +56,14 @@ public DeviceProfile() .ForMember(dest => dest.DeviceID, opts => opts.MapFrom(src => src.Id)) .ForMember(dest => dest.DeviceName, opts => opts.MapFrom(src => src.Name)) .ForMember(dest => dest.ModelId, opts => opts.MapFrom(src => src.DeviceModelId)) + .ForMember(dest => dest.LayerId, opts => opts.MapFrom(src => src.LayerId)) .ForMember(dest => dest.Tags, opts => opts.MapFrom(src => src.Tags.ToDictionary(tag => tag.Name, tag => tag.Value))); _ = 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.LayerId, opts => opts.MapFrom(src => src.Tags.Contains("layerId") ? src.Tags["layerId"] : null)) .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)) diff --git a/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs b/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs index 3d2df581d..616b53d7b 100644 --- a/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs +++ b/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs @@ -35,7 +35,8 @@ public DeviceDetails CreateDeviceDetails(Twin twin, IEnumerable tags) Image = this.deviceModelImageManager.GetDeviceModelImageAsync(modelId!).Result, IsConnected = twin.ConnectionState == DeviceConnectionState.Connected, IsEnabled = twin.Status == DeviceStatus.Enabled, - StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue + StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, + LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(DeviceDetails.LayerId)) }; foreach (var item in customTags) @@ -58,7 +59,8 @@ public DeviceListItem CreateDeviceListItem(Twin twin) StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, DeviceName = DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.DeviceName)), Image = this.deviceModelImageManager.GetDeviceModelImageAsync(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceDetails.ModelId))!).Result, - SupportLoRaFeatures = bool.Parse(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.SupportLoRaFeatures)) ?? "false") + SupportLoRaFeatures = bool.Parse(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.SupportLoRaFeatures)) ?? "false"), + LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.LayerId)) }; } @@ -70,6 +72,7 @@ public void UpdateTwin(Twin twin, DeviceDetails item) // Update the twin properties DeviceHelper.SetTagValue(twin, nameof(item.DeviceName), item.DeviceName); DeviceHelper.SetTagValue(twin, nameof(item.ModelId), item.ModelId); + DeviceHelper.SetTagValue(twin, nameof(item.LayerId), item.LayerId ?? string.Empty); if (item.Tags == null) { diff --git a/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs b/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs index 9669af7f8..16d1c000f 100644 --- a/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs +++ b/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs @@ -35,6 +35,7 @@ public LoRaDeviceDetails CreateDeviceDetails(Twin twin, IEnumerable tags IsConnected = twin.ConnectionState == DeviceConnectionState.Connected, IsEnabled = twin.Status == DeviceStatus.Enabled, StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, + LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(LoRaDeviceDetails.LayerId)), GatewayID = DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.GatewayID)), SensorDecoder = DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.SensorDecoder)), @@ -147,6 +148,7 @@ public DeviceListItem CreateDeviceListItem(Twin twin) .Result, IsConnected = twin.ConnectionState == DeviceConnectionState.Connected, IsEnabled = twin.Status == DeviceStatus.Enabled, + LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(LoRaDeviceDetails.LayerId)), StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, SupportLoRaFeatures = bool.Parse(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.SupportLoRaFeatures)) ?? @@ -164,6 +166,8 @@ public void UpdateTwin(Twin twin, LoRaDeviceDetails item) DeviceHelper.SetTagValue(twin, nameof(item.ModelId), item.ModelId); + DeviceHelper.SetTagValue(twin, nameof(item.LayerId), item.LayerId ?? string.Empty); + // Update OTAA settings DeviceHelper.SetDesiredProperty(twin, nameof(item.AppEUI), item.AppEUI); DeviceHelper.SetDesiredProperty(twin, nameof(item.AppKey), item.AppKey); From ee96eecd89b0fe15e754d944a352f3a4dfa61da0 Mon Sep 17 00:00:00 2001 From: E068097 Date: Mon, 14 Oct 2024 15:51:16 +0200 Subject: [PATCH 02/17] #2998 Quartz migration for SendPlanningCommand --- src/IoTHub.Portal.Domain/ConfigHandler.cs | 2 + .../ConfigHandlerBase.cs | 1 + .../DevelopmentConfigHandler.cs | 2 + .../Jobs/SendPlanningCommandJob.cs | 258 ++++++++++++++++++ .../ProductionAWSConfigHandler.cs | 2 + .../ProductionAzureConfigHandler.cs | 2 + .../AzureServiceCollectionExtension.cs | 17 +- src/IoTHub.Portal.Server/Startup.cs | 3 - 8 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs diff --git a/src/IoTHub.Portal.Domain/ConfigHandler.cs b/src/IoTHub.Portal.Domain/ConfigHandler.cs index 84616d7aa..c0a3cee83 100644 --- a/src/IoTHub.Portal.Domain/ConfigHandler.cs +++ b/src/IoTHub.Portal.Domain/ConfigHandler.cs @@ -83,5 +83,7 @@ public abstract class ConfigHandler public abstract string AWSAccountId { get; } public abstract string AWSGreengrassCoreTokenExchangeRoleAliasName { get; } public abstract IEnumerable AWSGreengrassRequiredRoles { get; } + + public abstract int SendCommandsToDevicesIntervalInMinutes { get; } } } diff --git a/src/IoTHub.Portal.Infrastructure/ConfigHandlerBase.cs b/src/IoTHub.Portal.Infrastructure/ConfigHandlerBase.cs index 69fbf3d25..067cb66b1 100644 --- a/src/IoTHub.Portal.Infrastructure/ConfigHandlerBase.cs +++ b/src/IoTHub.Portal.Infrastructure/ConfigHandlerBase.cs @@ -42,6 +42,7 @@ public abstract class ConfigHandlerBase : ConfigHandler public const string MetricLoaderRefreshIntervalKey = "Metrics:LoaderRefreshIntervalInMinutes"; public const string SyncDatabaseJobRefreshIntervalKey = "Job:SyncDatabaseJobRefreshIntervalInMinutes"; + public const string SendCommandsToDevicesIntervalKey = "Job:SendCommandsToDevicesIntervalInMinutes"; public const string IdeasEnabledKey = "Ideas:Enabled"; public const string IdeasUrlKey = "Ideas:Url"; diff --git a/src/IoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs b/src/IoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs index ca30f2c62..a46d23b74 100644 --- a/src/IoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs +++ b/src/IoTHub.Portal.Infrastructure/DevelopmentConfigHandler.cs @@ -89,5 +89,7 @@ public DevelopmentConfigHandler(IConfiguration config) public override string AWSAccountId => this.config[AWSAccountIdKey]!; public override IEnumerable AWSGreengrassRequiredRoles => this.config.GetSection(AWSGreengrassRequiredRolesKey).Get()!; public override string AWSGreengrassCoreTokenExchangeRoleAliasName => this.config[AWSGreengrassCoreTokenExchangeRoleAliasNameKey]!; + + public override int SendCommandsToDevicesIntervalInMinutes => this.config.GetValue(SendCommandsToDevicesIntervalKey, 10); } } diff --git a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs new file mode 100644 index 000000000..57a406441 --- /dev/null +++ b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs @@ -0,0 +1,258 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace IoTHub.Portal.Infrastructure.Jobs +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using System.Linq; + using System.Threading.Tasks; + using IoTHub.Portal.Application.Services; + using IoTHub.Portal.Models.v10; + using IoTHub.Portal.Shared.Constants; + using IoTHub.Portal.Shared.Models.v1._0; + using IoTHub.Portal.Shared.Models.v10; + using Microsoft.Extensions.Logging; + using Quartz; + + public class PlanningCommand + { + public string planningId { get; set; } = default!; + public Collection listDeviceId { get; } = new Collection(); + public Dictionary> commands { get; } = new Dictionary>(); + + public PlanningCommand(string listDeviceId, string planningId) + { + this.planningId = planningId; + this.listDeviceId.Add(listDeviceId); + + foreach (DaysEnumFlag.DaysOfWeek day in Enum.GetValues(typeof(DaysEnumFlag.DaysOfWeek))) + { + commands.Add(day, new List()); + } + } + } + + public class PayloadCommand + { + public string payloadId { get; set; } = default!; + public TimeSpan start { get; set; } = default!; + public TimeSpan end { get; set; } = default!; + + public PayloadCommand(TimeSpan start, TimeSpan end, string payloadId) + { + this.payloadId = payloadId; + this.start = start; + this.end = end; + } + } + + [DisallowConcurrentExecution] + public class SendPlanningCommandJob : IJob + { + private readonly IDeviceService deviceService; + private readonly ILayerService layerService; + private readonly IPlanningService planningService; + private readonly IScheduleService scheduleService; + private readonly ILoRaWANCommandService loRaWANCommandService; + + private readonly CancellationTokenSource cancellationTokenSource; + + private readonly List planningCommands = new List(); + public PaginatedResult devices { get; set; } = new PaginatedResult(); + public IEnumerable layers { get; set; } = new List(); + public IEnumerable plannings { get; set; } = new List(); + public IEnumerable schedules { get; set; } = new List(); + private readonly ILogger logger; + + public SendPlanningCommandJob(IDeviceService deviceService, + ILayerService layerService, + IPlanningService planningService, + IScheduleService scheduleService, + ILoRaWANCommandService loRaWANCommandService, + ILogger logger) + { + this.logger = logger; + + this.deviceService = deviceService; + this.layerService = layerService; + this.planningService = planningService; + this.scheduleService = scheduleService; + this.loRaWANCommandService = loRaWANCommandService; + + this.cancellationTokenSource = new CancellationTokenSource(); + } + + public async Task Execute(IJobExecutionContext context) + { + try + { + this.logger.LogInformation("Start of send planning commands job"); + + await DoWork(this.cancellationTokenSource.Token); + + this.logger.LogInformation("End of send planning commands job"); + } + catch (Exception e) + { + this.logger.LogError(e, "Send planning commands job has failed"); + } + } + + private async Task DoWork(CancellationToken stoppingToken) + { + if (stoppingToken.IsCancellationRequested) + { + return; + } + + try + { + this.planningCommands.Clear(); + await UpdateAPI(); + UpdateDatabase(); + + await SendCommand(); + } + catch (Exception e) + { + this.logger.LogError(e, "Send planning command has failed"); + } + } + + public async Task UpdateAPI() + { + try + { + devices = await this.deviceService.GetDevices(pageSize: 10000); + layers = await this.layerService.GetLayers(); + plannings = await this.planningService.GetPlannings(); + schedules = await this.scheduleService.GetSchedules(); + } + catch (Exception e) + { + this.logger.LogError(e, "Update API has failed"); + } + } + + public void UpdateDatabase() + { + foreach (var device in this.devices.Data) + { + if (device.LayerId != null) AddNewDevice(device); + } + } + + public void AddNewDevice(DeviceListItem device) + { + var layer = layers.FirstOrDefault(layer => layer.Id == device.LayerId); + + if (layer?.Planning != "None") + { + // If the layer linked to a device already has a planning, add the device to the planning list + foreach (var planning in this.planningCommands.Where(planning => planning.planningId == layer.Planning)) + { + planning.listDeviceId.Add(device.DeviceID); + return; + } + + // Else create the planning + var newPlanning = new PlanningCommand(device.DeviceID, layer.Planning); + AddCommand(newPlanning); + this.planningCommands.Add(newPlanning); + } + } + + public void AddCommand(PlanningCommand planningCommand) + { + var planningData = plannings.FirstOrDefault(planning => planning.Id == planningCommand.planningId); + + // Connect off days command to the planning + addPlanningSchedule(planningData, planningCommand); + + + foreach (var schedule in schedules) + { + // Add schedules to the planning + if (schedule.PlanningId == planningCommand.planningId) addSchedule(schedule, planningCommand); + } + + } + + // Include Planning Commands used for off days in the command dictionary. + // "Sa" represents Saturday and serves as a dictionary key. + // planning.commands[Sa] contains a list of PayloadCommand Values. + public void addPlanningSchedule(PlanningDto planningData, PlanningCommand planning) + { + foreach (var key in planning.commands.Keys) + { + if ((planningData.DayOff & key) == planningData.DayOff) + { + var newPayload = new PayloadCommand(getTimeSpan("0:00"), getTimeSpan("24:00"), planningData.CommandId); + planning.commands[key].Add(newPayload); + } + } + } + + public void addSchedule(ScheduleDto schedule, PlanningCommand planning) + { + // Convert a string into TimeSpan format + var start = getTimeSpan(schedule.Start); + var end = getTimeSpan(schedule.End); + + foreach (var key in planning.commands.Keys) + { + if (planning.commands[key].Count == 0) + { + var newPayload = new PayloadCommand(start, end, schedule.CommandId); + planning.commands[key].Add(newPayload); + } + // The if condition is utilized to skip day off schedules. + else if (planning.commands[key][0].start != getTimeSpan("00:00") || planning.commands[key][0].end != getTimeSpan("24:00")) + { + var newPayload = new PayloadCommand(start, end, schedule.CommandId); + planning.commands[key].Add(newPayload); + } + } + } + + public TimeSpan getTimeSpan(string time) + { + var tabTime = time != null ? time.Split(':') : ("0:0").Split(':'); + + var hour = int.Parse(tabTime[0], CultureInfo.InvariantCulture); + var minute = int.Parse(tabTime[1], CultureInfo.InvariantCulture); + + return new TimeSpan(hour, minute, 0); + } + + public async Task SendCommand() + { + var timeZoneId = "Europe/Paris"; + var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + var currentTime = TimeZoneInfo.ConvertTime(DateTime.Now, timeZone); + + var currentDay = currentTime.DayOfWeek; + var currentHour = currentTime.TimeOfDay ; + + // Search for the appropriate command at the correct time from each plan. + foreach (var planning in this.planningCommands) + { + foreach (var schedule in planning.commands[DayConverter.Convert(currentDay)]) + { + if (schedule.start < currentHour && schedule.end > currentHour) + { + await SendDevicesCommand(planning.listDeviceId, schedule.payloadId); + } + } + } + } + + public async Task SendDevicesCommand(Collection devices, string command) + { + foreach (var device in devices) await loRaWANCommandService.ExecuteLoRaWANCommand(device, command); + } + } +} diff --git a/src/IoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs b/src/IoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs index 4d979c95b..f5849ece9 100644 --- a/src/IoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs +++ b/src/IoTHub.Portal.Infrastructure/ProductionAWSConfigHandler.cs @@ -89,5 +89,7 @@ public ProductionAWSConfigHandler(IConfiguration config) public override string AWSAccountId => this.config[AWSAccountIdKey]!; public override IEnumerable AWSGreengrassRequiredRoles => this.config.GetSection(AWSGreengrassRequiredRolesKey).Get()!; public override string AWSGreengrassCoreTokenExchangeRoleAliasName => this.config[AWSGreengrassCoreTokenExchangeRoleAliasNameKey]!; + + public override int SendCommandsToDevicesIntervalInMinutes => this.config.GetValue(SendCommandsToDevicesIntervalKey, 10); } } diff --git a/src/IoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs b/src/IoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs index 62e6c5fbb..0bf4c4bc5 100644 --- a/src/IoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs +++ b/src/IoTHub.Portal.Infrastructure/ProductionAzureConfigHandler.cs @@ -79,6 +79,8 @@ public ProductionAzureConfigHandler(IConfiguration config) public override string IdeasAuthenticationHeader => this.config.GetValue(IdeasAuthenticationHeaderKey, "Ocp-Apim-Subscription-Key")!; public override string IdeasAuthenticationToken => this.config.GetValue(IdeasAuthenticationTokenKey, string.Empty)!; + public override int SendCommandsToDevicesIntervalInMinutes => this.config.GetValue(SendCommandsToDevicesIntervalKey, 10); + public override string CloudProvider => this.config[CloudProviderKey]!; public override string AWSAccess => throw new NotImplementedException(); diff --git a/src/IoTHub.Portal.Infrastructure/Startup/AzureServiceCollectionExtension.cs b/src/IoTHub.Portal.Infrastructure/Startup/AzureServiceCollectionExtension.cs index 00d01ac0d..d71f37ca3 100644 --- a/src/IoTHub.Portal.Infrastructure/Startup/AzureServiceCollectionExtension.cs +++ b/src/IoTHub.Portal.Infrastructure/Startup/AzureServiceCollectionExtension.cs @@ -14,7 +14,8 @@ public static IServiceCollection AddAzureInfrastructureLayer(this IServiceCollec .ConfigureMappers() .ConfigureHealthCheck() .ConfigureMetricsJobs(configuration) - .ConfigureSyncJobs(configuration); + .ConfigureSyncJobs(configuration) + .ConfigureSendingCommands(configuration); } private static IServiceCollection AddLoRaWanSupport(this IServiceCollection services, ConfigHandler configuration) @@ -164,5 +165,19 @@ private static IServiceCollection ConfigureSyncJobs(this IServiceCollection serv } }); } + + private static IServiceCollection ConfigureSendingCommands(this IServiceCollection services, ConfigHandler configuration) + { + return services.AddQuartz(q => + { + _ = q.AddJob(j => j.WithIdentity(nameof(SendPlanningCommandJob))) + .AddTrigger(t => t + .WithIdentity($"{nameof(SendPlanningCommandJob)}") + .ForJob(nameof(SendPlanningCommandJob)) + .WithSimpleSchedule(s => s + .WithIntervalInMinutes(configuration.SendCommandsToDevicesIntervalInMinutes) + .RepeatForever())); + }); + } } } diff --git a/src/IoTHub.Portal.Server/Startup.cs b/src/IoTHub.Portal.Server/Startup.cs index 36a851bc2..7403961df 100644 --- a/src/IoTHub.Portal.Server/Startup.cs +++ b/src/IoTHub.Portal.Server/Startup.cs @@ -200,9 +200,6 @@ Specify the authorization token got from your IDP as a header. opts.OrderActionsBy(api => api.RelativePath); opts.UseInlineDefinitionsForEnums(); }); - - _ = services.AddHostedService(); - _ = services.AddScoped(); _ = services.AddApiVersioning(o => { From ca29b95cb3ea46085111ea4d070f5cd4b781df04 Mon Sep 17 00:00:00 2001 From: E068097 Date: Tue, 15 Oct 2024 09:13:42 +0200 Subject: [PATCH 03/17] #2856 Disable built-in device model deletion --- .../Pages/DeviceModels/DeviceModelDetailPage.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor b/src/IoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor index 50990ffc8..5721e3c68 100644 --- a/src/IoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor +++ b/src/IoTHub.Portal.Client/Pages/DeviceModels/DeviceModelDetailPage.razor @@ -37,7 +37,7 @@ - Delete model + Delete model Save Changes From af9d7ebb517a9ffbc55a29ff0368d4fee09d842e Mon Sep 17 00:00:00 2001 From: E068097 Date: Tue, 15 Oct 2024 09:15:59 +0200 Subject: [PATCH 04/17] #3238 Update view when a device is unchecked --- .../Dialogs/Layer/LinkDeviceLayerDialog.razor | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor b/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor index 8e513ba00..09279b12f 100644 --- a/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor @@ -170,7 +170,12 @@ if (device.LayerId != null && device.LayerId == InitLayer.Id) { if (DeviceRemoveList.Contains(device.DeviceID)) DeviceRemoveList.Remove(device.DeviceID); - else DeviceRemoveList.Add(device.DeviceID); + else + { + DeviceList.Remove(device.DeviceID); + DeviceRemoveList.Add(device.DeviceID); + device.LayerId = null; + } } else { From a77e915518053df1ac3b5eafe04359514d5d22d0 Mon Sep 17 00:00:00 2001 From: E068097 Date: Tue, 15 Oct 2024 14:33:31 +0200 Subject: [PATCH 05/17] 3239 Allow to delete a planning from client --- .../Components/Planning/EditPlanning.razor | 33 +++++++++++- .../Planning/DeletePlanningDialog.razor | 51 +++++++++++++++++++ .../Pages/Planning/PlanningListPage.razor | 45 ++++++++++++++++ 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor diff --git a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor index 7773dda96..dcfaf1def 100644 --- a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor +++ b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor @@ -1,4 +1,5 @@ -@using IoTHub.Portal.Models +@using IoTHub.Portal.Client.Dialogs.Planning +@using IoTHub.Portal.Models @using IoTHub.Portal.Models.v10 @using IoTHub.Portal.Shared.Models.v10 @using IoTHub.Portal.Client.Validators @@ -25,6 +26,10 @@ @mode Planning Save + @if (mode == "Edit") + { + Delete planning + } @if (!isProcessing) { @@ -418,4 +423,30 @@ return ""; } + /// + /// Prompts a pop-up windows to confirm the planning's deletion. + /// + /// + private async Task DeletePlanning() + { + isProcessing = true; + + var parameters = new DialogParameters + { + {"planningID", planning.Id}, + {"planningName", planning.Name} + }; + var result = await DialogService.Show("Confirm Deletion", parameters).Result; + + isProcessing = false; + + if (result.Canceled) + { + return; + } + + // Go back to the list of plannings + NavigationManager.NavigateTo($"/planning"); + } + } diff --git a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor new file mode 100644 index 000000000..8019b293e --- /dev/null +++ b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor @@ -0,0 +1,51 @@ +@inject ISnackbar Snackbar +@inject IPlanningClientService PlanningClientService + + + + + + + Delete @planningName ? + + + + Warning : this cannot be undone. + + + + + Cancel + Delete + + + +@code { + [CascadingParameter] + public Error Error { get; set; } = default!; + + [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] public string planningID { get; set; } = default!; + [Parameter] public string planningName { get; set; } = default!; + + void Submit() => MudDialog.Close(DialogResult.Ok(true)); + void Cancel() => MudDialog.Cancel(); + + private async Task DeletePlanning() + { + try + { + await PlanningClientService.DeletePlanning(planningID); + + Snackbar.Add($"Planning {planningName} has been successfully deleted!", Severity.Success); + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + } + finally + { + MudDialog.Close(); + } + } +} diff --git a/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor b/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor index f50a5da91..5ced29918 100644 --- a/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor +++ b/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor @@ -1,5 +1,24 @@ @page "/planning" +@using IoTHub.Portal +@using IoTHub.Portal.Client.Dialogs.Planning +@using IoTHub.Portal.Models.v10 +@using IoTHub.Portal.Shared.Models.v10; +@using IoTHub.Portal.Client.Dialogs.Layer +@using System.Web +@using System.IO +@using System.Text.RegularExpressions +@using System.Net.Http.Headers +@using IoTHub.Portal.Shared.Models.v10.Filters +@using IoTHub.Portal.Client.Enums +@using IoTHub.Portal.Client.Pages.Devices +@using IoTHub.Portal.Client.Validators +@using IoTHub.Portal.Shared.Constants; +@using IoTHub.Portal.Shared.Models +@using IoTHub.Portal.Models +@using IoTHub.Portal.Models.v10.LoRaWAN +@using Microsoft.AspNetCore.WebUtilities + @attribute [Authorize] @inject NavigationManager navigationManager @@ -33,6 +52,7 @@ End Frequency Detail + Delete @@ -54,6 +74,11 @@ + + + + + @@ -115,4 +140,24 @@ { navigationManager.NavigateTo($"/planning/new"); } + + /// + /// Prompts a pop-up windows to confirm the planning's deletion. + /// + /// + private async Task DeletePlanning(PlanningDto item) + { + var parameters = new DialogParameters(); + parameters.Add("planningID", item.Id); + parameters.Add("planningName", item.Name); + + var result = await DialogService.Show("Confirm Deletion", parameters).Result; + + if (result.Canceled) + { + return; + } + + await GetPlannings(); + } } From 46bbaedcd1353dbad772555eda59cb13896da1bd Mon Sep 17 00:00:00 2001 From: E068097 Date: Mon, 28 Oct 2024 17:06:54 +0100 Subject: [PATCH 06/17] 3239 Allow to delete a planning --- .../Planning/DeletePlanningDialog.razor | 41 +- .../Pages/Layer/LayerListPage.razor | 2 +- src/IoTHub.Portal.Domain/Entities/Planning.cs | 9 + .../PortalDbContext.cs | 5 + ... cascade plannings & schedules.Designer.cs | 712 ++++++++++++++++++ ...31_Delete cascade plannings & schedules.cs | 42 ++ .../PortalDbContextModelSnapshot.cs | 18 +- 7 files changed, 826 insertions(+), 3 deletions(-) create mode 100644 src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.Designer.cs create mode 100644 src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.cs diff --git a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor index 8019b293e..79995d11e 100644 --- a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor @@ -1,5 +1,8 @@ -@inject ISnackbar Snackbar +@using IoTHub.Portal.Shared.Models.v10 +@inject ISnackbar Snackbar @inject IPlanningClientService PlanningClientService +@inject ILayerClientService LayerClientService +@inject IScheduleClientService ScheduleClientService @@ -28,13 +31,21 @@ [Parameter] public string planningID { get; set; } = default!; [Parameter] public string planningName { get; set; } = default!; + List Layers { get; set; } = new List(); + void Submit() => MudDialog.Close(DialogResult.Ok(true)); void Cancel() => MudDialog.Cancel(); + protected override async Task OnInitializedAsync() + { + Layers = await LayerClientService.GetLayers(); + } + private async Task DeletePlanning() { try { + await DeletePlanningOnLayer(planningID); await PlanningClientService.DeletePlanning(planningID); Snackbar.Add($"Planning {planningName} has been successfully deleted!", Severity.Success); @@ -48,4 +59,32 @@ MudDialog.Close(); } } + + private async Task DeletePlanningOnLayer(string planningId) + { + foreach (var layer in Layers.Where(layer => layer.Planning == planningId)) + { + var updatedLayer = FindLayer(layer.Id); + updatedLayer.Planning = null; + await LayerClientService.UpdateLayer(updatedLayer); + } + } + + private LayerDto FindLayer(string layerId) + { + var layer = Layers.FirstOrDefault(layer => layer.Id == layerId); + + var layerDto = new LayerDto(); + + if (layer == null) return layerDto; + + layerDto.Id = layer.Id; + layerDto.Name = layer.Name; + layerDto.Father = layer.Father; + layerDto.Planning = layer.Planning; + layerDto.hasSub = layer.hasSub; + + return layerDto; + } + } diff --git a/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor b/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor index 80347c0b0..72b6ff743 100644 --- a/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor +++ b/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor @@ -80,7 +80,7 @@ { Name = "New Layer", Father = layer.LayerData.Id, - Planning = "None" + Planning = layer.LayerData.Planning != null ? layer.LayerData.Planning : "None" }; newLayer.Id = await LayerClientService.CreateLayer(newLayer); diff --git a/src/IoTHub.Portal.Domain/Entities/Planning.cs b/src/IoTHub.Portal.Domain/Entities/Planning.cs index f834997fe..194900c7d 100644 --- a/src/IoTHub.Portal.Domain/Entities/Planning.cs +++ b/src/IoTHub.Portal.Domain/Entities/Planning.cs @@ -3,6 +3,10 @@ namespace IoTHub.Portal.Domain.Entities { + using System.Collections.ObjectModel; + using IoTHub.Portal.Domain.Base; + using IoTHub.Portal.Shared.Constants; + public class Planning : EntityBase { /// @@ -34,5 +38,10 @@ public class Planning : EntityBase /// Day off command. /// public string CommandId { get; set; } = default!; + + /// + /// Gets or sets the schedules. + /// + public ICollection Schedules { get; set; } = new Collection(); } } diff --git a/src/IoTHub.Portal.Infrastructure/PortalDbContext.cs b/src/IoTHub.Portal.Infrastructure/PortalDbContext.cs index 67f65ff44..b154ca284 100644 --- a/src/IoTHub.Portal.Infrastructure/PortalDbContext.cs +++ b/src/IoTHub.Portal.Infrastructure/PortalDbContext.cs @@ -66,6 +66,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasMany(c => c.Tags) .WithOne() .OnDelete(DeleteBehavior.Cascade); + + _ = modelBuilder.Entity() + .HasMany(c => c.Schedules) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.Designer.cs b/src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.Designer.cs new file mode 100644 index 000000000..9c8ede4fb --- /dev/null +++ b/src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.Designer.cs @@ -0,0 +1,712 @@ +// +using System; +using IoTHub.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 IoTHub.Portal.Postgres.Migrations +{ + [DbContext(typeof(PortalDbContext))] + [Migration("20241018120631_Delete cascade plannings & schedules")] + partial class Deletecascadeplanningsschedules + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Concentrator", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClientThumbprint") + .HasColumnType("text"); + + b.Property("DeviceType") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConnected") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LoraRegion") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Concentrators"); + }); + + modelBuilder.Entity("IoTHub.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("LayerId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("Devices", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ABPRelaxMode") + .HasColumnType("boolean"); + + b.Property("AppEUI") + .HasColumnType("text"); + + b.Property("ClassType") + .HasColumnType("integer"); + + 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("IoTHub.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("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.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("IoTHub.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("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("EdgeDeviceId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("EdgeDeviceId"); + + b.ToTable("DeviceTagValues"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConnectionState") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NbDevices") + .HasColumnType("integer"); + + b.Property("NbModules") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("EdgeDevices"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExternalIdentifier") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("EdgeDeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModuleName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeviceModelId") + .HasColumnType("text"); + + b.Property("EdgeDeviceId") + .HasColumnType("text"); + + b.Property("EdgeDeviceModelId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("DeviceModelId"); + + b.HasIndex("EdgeDeviceId"); + + b.HasIndex("EdgeDeviceModelId"); + + b.ToTable("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Layer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Father") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Planning") + .IsRequired() + .HasColumnType("text"); + + b.Property("hasSub") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Layers"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("EnqueuedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LorawanDeviceId") + .HasColumnType("text"); + + b.Property("Telemetry") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LorawanDeviceId"); + + b.ToTable("LoRaDeviceTelemetry"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DayOff") + .HasColumnType("integer"); + + b.Property("End") + .IsRequired() + .HasColumnType("text"); + + b.Property("Frequency") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Start") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Plannings"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("text"); + + b.Property("End") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Start") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PlanningId"); + + b.ToTable("Schedules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasBaseType("IoTHub.Portal.Domain.Entities.Device"); + + 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("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("KeepAliveTimeout") + .HasColumnType("integer"); + + 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("Supports32BitFCnt") + .HasColumnType("boolean"); + + b.Property("TxPower") + .HasColumnType("text"); + + b.Property("UseOTAA") + .HasColumnType("boolean"); + + b.ToTable("LorawanDevices", (string)null); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Tags") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Tags") + .HasForeignKey("EdgeDeviceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Labels") + .HasForeignKey("DeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", null) + .WithMany("Labels") + .HasForeignKey("DeviceModelId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceModelId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.LorawanDevice", null) + .WithMany("Telemetry") + .HasForeignKey("LorawanDeviceId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Planning", null) + .WithMany("Schedules") + .HasForeignKey("PlanningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithOne() + .HasForeignKey("IoTHub.Portal.Domain.Entities.LorawanDevice", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.Navigation("Telemetry"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.cs b/src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.cs new file mode 100644 index 000000000..08b1c1281 --- /dev/null +++ b/src/IoTHub.Portal.Postgres/Migrations/20241018120631_Delete cascade plannings & schedules.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. + +#nullable disable + +namespace IoTHub.Portal.Postgres.Migrations +{ + using Microsoft.EntityFrameworkCore.Migrations; + + /// + public partial class Deletecascadeplanningsschedules : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.CreateIndex( + name: "IX_Schedules_PlanningId", + table: "Schedules", + column: "PlanningId"); + + _ = migrationBuilder.AddForeignKey( + name: "FK_Schedules_Plannings_PlanningId", + table: "Schedules", + column: "PlanningId", + principalTable: "Plannings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropForeignKey( + name: "FK_Schedules_Plannings_PlanningId", + table: "Schedules"); + + _ = migrationBuilder.DropIndex( + name: "IX_Schedules_PlanningId", + table: "Schedules"); + } + } +} diff --git a/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs b/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs index c47d0c7dd..31f1eefa1 100644 --- a/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs +++ b/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -476,6 +476,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("PlanningId"); + b.ToTable("Schedules"); }); @@ -650,6 +652,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("LorawanDeviceId"); }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Planning", null) + .WithMany("Schedules") + .HasForeignKey("PlanningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => { b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) @@ -683,6 +694,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Labels"); }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Navigation("Schedules"); + }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => { b.Navigation("Telemetry"); From d223fe4b806ea2ed9951b4caca75e54e30dfa4b0 Mon Sep 17 00:00:00 2001 From: E068097 Date: Thu, 31 Oct 2024 11:28:12 +0100 Subject: [PATCH 07/17] 2998 Schedule commands --- .../Services/ISendPlanningCommandService.cs | 9 - .../Jobs/SendPlanningCommandJob.cs | 36 +- .../Services/SendPlanningCommandService.cs | 325 ------------------ .../Jobs/SendPlanningCommandJobTests.cs | 203 +++++++++++ 4 files changed, 228 insertions(+), 345 deletions(-) delete mode 100644 src/IoTHub.Portal.Application/Services/ISendPlanningCommandService.cs delete mode 100644 src/IoTHub.Portal.Server/Services/SendPlanningCommandService.cs create mode 100644 src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs diff --git a/src/IoTHub.Portal.Application/Services/ISendPlanningCommandService.cs b/src/IoTHub.Portal.Application/Services/ISendPlanningCommandService.cs deleted file mode 100644 index 26d2d24b8..000000000 --- a/src/IoTHub.Portal.Application/Services/ISendPlanningCommandService.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) CGI France. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace IoTHub.Portal.Application.Services -{ - public interface ISendPlanningCommandService - { - } -} diff --git a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs index 57a406441..44ca1184e 100644 --- a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs +++ b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs @@ -141,7 +141,7 @@ public void UpdateDatabase() { foreach (var device in this.devices.Data) { - if (device.LayerId != null) AddNewDevice(device); + if (!string.IsNullOrWhiteSpace(device.LayerId)) AddNewDevice(device); } } @@ -169,16 +169,27 @@ public void AddCommand(PlanningCommand planningCommand) { var planningData = plannings.FirstOrDefault(planning => planning.Id == planningCommand.planningId); - // Connect off days command to the planning - addPlanningSchedule(planningData, planningCommand); + // If planning is active + if (planningData != null && IsPlanningActive(planningData)) + { + // Connect off days command to the planning + addPlanningSchedule(planningData, planningCommand); - foreach (var schedule in schedules) - { - // Add schedules to the planning - if (schedule.PlanningId == planningCommand.planningId) addSchedule(schedule, planningCommand); + foreach (var schedule in schedules) + { + // Add schedules to the planning + if (schedule.PlanningId == planningCommand.planningId) addSchedule(schedule, planningCommand); + } } + } + private bool IsPlanningActive(PlanningDto planning) + { + var startDay = DateTime.ParseExact(planning.Start, "yyyy-MM-dd", CultureInfo.InvariantCulture); + var endDay = DateTime.ParseExact(planning.End, "yyyy-MM-dd", CultureInfo.InvariantCulture); + + return DateTime.Now >= startDay && DateTime.Now <= endDay; } // Include Planning Commands used for off days in the command dictionary. @@ -186,12 +197,15 @@ public void AddCommand(PlanningCommand planningCommand) // planning.commands[Sa] contains a list of PayloadCommand Values. public void addPlanningSchedule(PlanningDto planningData, PlanningCommand planning) { - foreach (var key in planning.commands.Keys) + if (planningData != null) { - if ((planningData.DayOff & key) == planningData.DayOff) + foreach (var key in planning.commands.Keys) { - var newPayload = new PayloadCommand(getTimeSpan("0:00"), getTimeSpan("24:00"), planningData.CommandId); - planning.commands[key].Add(newPayload); + if ((planningData.DayOff & key) == planningData.DayOff) + { + var newPayload = new PayloadCommand(getTimeSpan("0:00"), getTimeSpan("24:00"), planningData.CommandId); + planning.commands[key].Add(newPayload); + } } } } diff --git a/src/IoTHub.Portal.Server/Services/SendPlanningCommandService.cs b/src/IoTHub.Portal.Server/Services/SendPlanningCommandService.cs deleted file mode 100644 index 946b37580..000000000 --- a/src/IoTHub.Portal.Server/Services/SendPlanningCommandService.cs +++ /dev/null @@ -1,325 +0,0 @@ -// Copyright (c) CGI France. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace IoTHub.Portal.Server.Services -{ - using ProblemDetailsException = Client.Exceptions.ProblemDetailsException; - - public class PlanningCommand - { - public string planningId { get; set; } = default!; - public Collection listDeviceId { get; } = new Collection(); - public Dictionary> commands { get; } = new Dictionary>(); - - - public PlanningCommand(string listDeviceId, string planningId) - { - this.planningId = planningId; - this.listDeviceId.Add(listDeviceId); - - foreach (DaysEnumFlag.DaysOfWeek day in Enum.GetValues(typeof(DaysEnumFlag.DaysOfWeek))) - { - commands.Add(day, new List()); - } - } - } - - public class PayloadCommand - { - public string payloadId { get; set; } = default!; - public TimeSpan start { get; set; } = default!; - public TimeSpan end { get; set; } = default!; - - public PayloadCommand(TimeSpan start, TimeSpan end, string payloadId) - { - this.payloadId = payloadId; - this.start = start; - this.end = end; - } - } - - public class SendPlanningCommandService : ISendPlanningCommandService, IHostedService, IDisposable - { - [CascadingParameter] - private Error Error { get; set; } = default!; - - private readonly CancellationTokenSource cancellationTokenSource; - private bool isUpdating; - - private readonly List planningCommands = new List(); - - private readonly IDeviceService deviceService; - private readonly ILayerService layerService; - private readonly IPlanningService planningService; - private readonly IScheduleService scheduleService; - private readonly ILoRaWANCommandService loRaWANCommandService; - - public PaginatedResult devices { get; set; } = new PaginatedResult(); - public IEnumerable layers { get; set; } = new List(); - public IEnumerable plannings { get; set; } = new List(); - public IEnumerable schedules { get; set; } = new List(); - - /// - /// The logger. - /// - private readonly ILogger logger; - - /// - /// The service scope. - /// - private readonly IServiceScope serviceScope; - - /// - /// The timer period. - /// - private readonly TimeSpan timerPeriod; - - /// - /// The timer. - /// - private Timer timer; - - /// - /// The executing task. - /// - private Task executingTask; - - public SendPlanningCommandService( - ILogger logger, - IServiceProvider serviceProvider) - { - this.logger = logger; - - this.serviceScope = serviceProvider.CreateScope(); - - this.deviceService = this.serviceScope.ServiceProvider.GetRequiredService>(); - this.layerService = this.serviceScope.ServiceProvider.GetRequiredService(); - this.planningService = this.serviceScope.ServiceProvider.GetRequiredService(); - this.scheduleService = this.serviceScope.ServiceProvider.GetRequiredService(); - this.loRaWANCommandService = this.serviceScope.ServiceProvider.GetRequiredService(); - - this.cancellationTokenSource = new CancellationTokenSource(); - - var timeSpanSeconds = 600; - this.timerPeriod = TimeSpan.FromSeconds(timeSpanSeconds); - this.isUpdating = true; - } - - /// - /// Triggered when the application host is ready to start the service. - /// - /// Indicates that the start process has been aborted. - /// - /// Async task. - /// - public async Task StartAsync(CancellationToken cancellationToken) - { - // Create the timer - this.timer = new Timer(this.OnTimerCallback, null, TimeSpan.Zero, this.timerPeriod); - } - - /// - /// Does the work asynchronous. - /// - /// The stopping token. - private async Task DoWorkAsync(CancellationToken stoppingToken) - { - if (stoppingToken.IsCancellationRequested) - { - return; - } - - try - { - if (this.isUpdating) - { - this.planningCommands.Clear(); - await UpdateAPI(); - UpdateDatabase(); - this.isUpdating = false; - } - await SendCommand(); - } - catch (ProblemDetailsException exception) - { - Error?.ProcessProblemDetails(exception); - } - - _ = this.timer.Change(this.timerPeriod, TimeSpan.FromMilliseconds(-1)); - } - - /// - /// Triggered when the application host is performing a graceful shutdown. - /// - /// Indicates that the shutdown process should no longer be graceful. - /// - /// Async task. - /// - public async Task StopAsync(CancellationToken cancellationToken) - { - _ = (this.timer?.Change(Timeout.Infinite, 0)); - } - - /// - /// Called when [timer callback]. - /// - /// The state. - private void OnTimerCallback(object state) - { - GC.KeepAlive(this.timer); - _ = (this.timer?.Change(Timeout.Infinite, 0)); - this.executingTask = this.DoWorkAsync(this.cancellationTokenSource.Token); - } - - public async Task UpdateAPI() - { - try - { - devices = await this.deviceService.GetDevices(); - layers = await this.layerService.GetLayers(); - plannings = await this.planningService.GetPlannings(); - schedules = await this.scheduleService.GetSchedules(); - } - catch (ProblemDetailsException exception) - { - Error?.ProcessProblemDetails(exception); - } - } - - public void UpdateDatabase() - { - foreach (var device in this.devices.Data) - { - if (device.LayerId != null) AddNewDevice(device); - } - } - - public void AddNewDevice(DeviceListItem device) - { - var layer = layers.FirstOrDefault(layer => layer.Id == device.LayerId); - - // If the layer linked to a device already has a planning, add the device to the planning list - foreach (var planning in this.planningCommands.Where(planning => planning.planningId == layer.Planning)) - { - planning.listDeviceId.Add(device.DeviceID); - return; - } - - // Else create the planning - var newPlanning = new PlanningCommand(device.DeviceID, layer.Planning); - AddCommand(newPlanning); - this.planningCommands.Add(newPlanning); - } - - public void AddCommand(PlanningCommand planningCommand) - { - var planningData = plannings.FirstOrDefault(planning => planning.Id == planningCommand.planningId); - - // Connect off days command to the planning - addPlanningSchedule(planningData, planningCommand); - - - foreach (var schedule in schedules) - { - // Add schedules to the planning - if (schedule.PlanningId == planningCommand.planningId) addSchedule(schedule, planningCommand); - } - - } - - // Include Planning Commands used for off days in the command dictionary. - // "Sa" represents Saturday and serves as a dictionary key. - // planning.commands[Sa] contains a list of PayloadCommand Values. - public void addPlanningSchedule(PlanningDto planningData, PlanningCommand planning) - { - foreach (var key in planning.commands.Keys) - { - if ((planningData.DayOff & key) == planningData.DayOff) - { - var newPayload = new PayloadCommand(getTimeSpan("0:00"), getTimeSpan("24:00"), planningData.CommandId); - planning.commands[key].Add(newPayload); - } - } - } - - public void addSchedule(ScheduleDto schedule, PlanningCommand planning) - { - // Convert a string into TimeSpan format - var start = getTimeSpan(schedule.Start); - var end = getTimeSpan(schedule.End); - - foreach (var key in planning.commands.Keys) - { - if (planning.commands[key].Count == 0) - { - var newPayload = new PayloadCommand(start, end, schedule.CommandId); - planning.commands[key].Add(newPayload); - } - // The if condition is utilized to skip day off schedules. - else if (planning.commands[key][0].start != getTimeSpan("00:00") || planning.commands[key][0].end != getTimeSpan("24:00")) - { - var newPayload = new PayloadCommand(start, end, schedule.CommandId); - planning.commands[key].Add(newPayload); - } - } - } - - public TimeSpan getTimeSpan(string time) - { - var tabTime = time != null ? time.Split(':') : ("0:0").Split(':'); - - var hour = int.Parse(tabTime[0], CultureInfo.InvariantCulture); - var minute = int.Parse(tabTime[1], CultureInfo.InvariantCulture); - - return new TimeSpan(hour, minute, 0); - } - - public async Task SendCommand() - { - var timeZoneId = "Europe/Paris"; - var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); - var currentTime = TimeZoneInfo.ConvertTime(DateTime.Now, timeZone); - - var currentDay = currentTime.DayOfWeek; - var currentHour = currentTime.TimeOfDay ; - - // Search for the appropriate command at the correct time from each plan. - foreach (var planning in this.planningCommands) - { - foreach (var schedule in planning.commands[DayConverter.Convert(currentDay)]) - { - if (schedule.start < currentHour && schedule.end > currentHour) - { - await SendDevicesCommand(planning.listDeviceId, schedule.payloadId); - } - } - } - } - - public async Task SendDevicesCommand(Collection devices, string command) - { - foreach (var device in devices) await loRaWANCommandService.ExecuteLoRaWANCommand(device, command); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool disposing) - { - this.timer?.Dispose(); - this.timer = null; - - this.cancellationTokenSource?.Dispose(); - } - } -} diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs new file mode 100644 index 000000000..cbea9d38f --- /dev/null +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs @@ -0,0 +1,203 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace IoTHub.Portal.Tests.Unit.Infrastructure.Jobs +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using IoTHub.Portal.Infrastructure.Jobs; + using Microsoft.Extensions.Logging; + using Moq; + using Quartz; + + [TestFixture] + public class SendPlanningCommandJobTests : BackendUnitTest + { + private SendPlanningCommandJob sendPlanningCommandJob; + + private MockRepository mockRepository; + private Mock> mockDeviceService; + private Mock mockLayerService; + private Mock mockPlanningService; + private Mock mockScheduleService; + private Mock mockLoraWANCommandService; + private Mock> mockLogger; + + [SetUp] + public void SetUp() + { + this.mockRepository = new MockRepository(MockBehavior.Strict); + + this.mockDeviceService = this.mockRepository.Create>(); + this.mockLayerService = this.mockRepository.Create(); + + this.mockPlanningService = this.MockRepository.Create(); + this.mockScheduleService = this.MockRepository.Create(); + this.mockLoraWANCommandService = this.MockRepository.Create(); + + this.mockLogger = this.mockRepository.Create>(); + + this.sendPlanningCommandJob = + new SendPlanningCommandJob(this.mockDeviceService.Object, this.mockLayerService.Object, this.mockPlanningService.Object, + this.mockScheduleService.Object, this.mockLoraWANCommandService.Object, this.mockLogger.Object); + } + + + [Test] + public async Task Execute_PlanningActive_ShouldSendCommandToDevice() + { + // Arrange + var mockJobExecutionContext = MockRepository.Create(); + + _ = this.mockLogger.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); + + var commands = new List + { + new DeviceModelCommandDto + { + Id = Guid.NewGuid().ToString() + } + }; + + var plannings = new List + { + new PlanningDto + { + Id = Guid.NewGuid().ToString(), + Start = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + End = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + DayOff = 0, + CommandId = commands.Single().Id + } + }; + + var layers = new List + { + new LayerDto + { + Id = Guid.NewGuid().ToString(), + Planning = plannings.Single().Id + } + }; + + var schedules = new List + { + new ScheduleDto + { + Id = Guid.NewGuid().ToString(), + Start = "00:00", + End = "00:00:00", + CommandId = commands.Single().Id, + PlanningId = plannings.Single().Id + } + }; + + var device = new DeviceListItem + { + DeviceID = Guid.NewGuid().ToString(), + LayerId = layers.Single().Id + }; + + var expectedPaginatedDevices = new PaginatedResult() + { + Data = Enumerable.Range(0, 1).Select(x => device).ToList(), + TotalCount = 1 + }; + + _ = this.mockDeviceService.Setup(service => service.GetDevices(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny(), + It.IsAny>(), It.IsAny())) + .ReturnsAsync(expectedPaginatedDevices); + + _ = this.mockLayerService.Setup(x => x.GetLayers()).ReturnsAsync(layers); + _ = this.mockPlanningService.Setup(x => x.GetPlannings()).ReturnsAsync(plannings); + _ = this.mockScheduleService.Setup(x => x.GetSchedules()).ReturnsAsync(schedules); + + // Act + await this.sendPlanningCommandJob.Execute(mockJobExecutionContext.Object); + + // Assert + MockRepository.VerifyAll(); + } + + [Test] + public async Task Execute_PlanningInactive_ShouldNotSendCommandToDevice() + { + // Arrange + var mockJobExecutionContext = MockRepository.Create(); + + _ = this.mockLogger.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); + + var commands = new List + { + new DeviceModelCommandDto + { + Id = Guid.NewGuid().ToString() + } + }; + + var plannings = new List + { + new PlanningDto + { + Id = Guid.NewGuid().ToString(), + Start = DateTime.Now.AddDays(-10).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + End = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + DayOff = 0, + CommandId = commands.Single().Id + } + }; + + var layers = new List + { + new LayerDto + { + Id = Guid.NewGuid().ToString(), + Planning = plannings.Single().Id + } + }; + + var schedules = new List + { + new ScheduleDto + { + Id = Guid.NewGuid().ToString(), + Start = "00:00", + End = "00:00:00", + CommandId = commands.Single().Id, + PlanningId = plannings.Single().Id + } + }; + + var device = new DeviceListItem + { + DeviceID = Guid.NewGuid().ToString(), + LayerId = layers.Single().Id + }; + + var expectedPaginatedDevices = new PaginatedResult() + { + Data = Enumerable.Range(0, 1).Select(x => device).ToList(), + TotalCount = 1 + }; + + _ = this.mockDeviceService.Setup(service => service.GetDevices(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny>(), It.IsAny(), + It.IsAny>(), It.IsAny())) + .ReturnsAsync(expectedPaginatedDevices); + + _ = this.mockLayerService.Setup(x => x.GetLayers()).ReturnsAsync(layers); + _ = this.mockPlanningService.Setup(x => x.GetPlannings()).ReturnsAsync(plannings); + _ = this.mockScheduleService.Setup(x => x.GetSchedules()).ReturnsAsync(schedules); + + // Act + await this.sendPlanningCommandJob.Execute(mockJobExecutionContext.Object); + + // Assert + MockRepository.VerifyAll(); + } + } +} From 00c8f0e78cdb26987b619f6b31d3be951f571b35 Mon Sep 17 00:00:00 2001 From: E068097 Date: Thu, 31 Oct 2024 11:30:34 +0100 Subject: [PATCH 08/17] #3239 Change checkboxes for layers displayed --- .../Components/Planning/EditPlanning.razor | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor index dcfaf1def..81000f9a3 100644 --- a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor +++ b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor @@ -192,7 +192,26 @@ - + + @if ((context.LayerData.Planning != null && context.LayerData.Planning != "None" && context.LayerData.Planning == planning.Id)) + { + + + + } + else if (context.LayerData.Planning != null && context.LayerData.Planning != "None" && context.LayerData.Planning != planning.Id) + { + + + + } + else + { + + + + } + @context.LayerData.Name From 48c28a6c61d9dabd904597ade89261df4b140dd7 Mon Sep 17 00:00:00 2001 From: E068097 Date: Thu, 31 Oct 2024 15:59:33 +0100 Subject: [PATCH 09/17] Merge from main --- .../Components/Planning/EditPlanning.razor | 15 +------------ .../Planning/DeletePlanningDialog.razor | 3 +-- src/IoTHub.Portal.Client/GlobalUsings.cs | 2 +- .../Pages/Planning/PlanningListPage.razor | 21 +------------------ src/IoTHub.Portal.Client/_Imports.razor | 1 + src/IoTHub.Portal.Domain/Entities/Planning.cs | 4 ---- .../Options/DeviceModelImageOptions.cs | 2 +- .../GlobalUsings.cs | 8 +------ .../Jobs/SendPlanningCommandJob.cs | 14 ------------- .../Startup/IServiceCollectionExtension.cs | 6 ++++++ src/IoTHub.Portal.Server/GlobalUsings.cs | 4 ---- .../Jobs/SendPlanningCommandJobTests.cs | 8 ------- 12 files changed, 13 insertions(+), 75 deletions(-) diff --git a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor index 81000f9a3..f52901c8f 100644 --- a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor +++ b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor @@ -1,17 +1,4 @@ -@using IoTHub.Portal.Client.Dialogs.Planning -@using IoTHub.Portal.Models -@using IoTHub.Portal.Models.v10 -@using IoTHub.Portal.Shared.Models.v10 -@using IoTHub.Portal.Client.Validators -@using System.Net.Http.Headers -@using IoTHub.Portal.Shared.Constants -@using IoTHub.Portal.Client.Models -@using IoTHub.Portal.Shared.Models -@using IoTHub.Portal.Shared.Models.v10.Filters -@using IoTHub.Portal.Models.v10.LoRaWAN -@using IoTHub.Portal.Client.Helpers - -@attribute [Authorize] +@attribute [Authorize] @inject NavigationManager NavigationManager @inject PortalSettings Portal diff --git a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor index 79995d11e..80675b852 100644 --- a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor @@ -1,5 +1,4 @@ -@using IoTHub.Portal.Shared.Models.v10 -@inject ISnackbar Snackbar +@inject ISnackbar Snackbar @inject IPlanningClientService PlanningClientService @inject ILayerClientService LayerClientService @inject IScheduleClientService ScheduleClientService diff --git a/src/IoTHub.Portal.Client/GlobalUsings.cs b/src/IoTHub.Portal.Client/GlobalUsings.cs index 665fd46d5..2f135400c 100644 --- a/src/IoTHub.Portal.Client/GlobalUsings.cs +++ b/src/IoTHub.Portal.Client/GlobalUsings.cs @@ -7,6 +7,7 @@ global using FluentValidation; global using IoTHub.Portal.Client; global using IoTHub.Portal.Client.Constants; +global using IoTHub.Portal.Client.Dialogs.Planning; global using IoTHub.Portal.Client.Exceptions; global using IoTHub.Portal.Client.Handlers; global using IoTHub.Portal.Client.Models; @@ -32,7 +33,6 @@ global using System.Net; global using IoTHub.Portal.Shared.Models.v10.Filters; global using Microsoft.AspNetCore.Components; -global using IoTHub.Portal.Shared.Models.v10.Filters; global using Microsoft.AspNetCore.WebUtilities; global using IoTHub.Portal.Shared.Models.v10.LoRaWAN; global using IoTHub.Portal.Shared.Constants; diff --git a/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor b/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor index 5ced29918..25e3ef298 100644 --- a/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor +++ b/src/IoTHub.Portal.Client/Pages/Planning/PlanningListPage.razor @@ -1,24 +1,5 @@ @page "/planning" -@using IoTHub.Portal -@using IoTHub.Portal.Client.Dialogs.Planning -@using IoTHub.Portal.Models.v10 -@using IoTHub.Portal.Shared.Models.v10; -@using IoTHub.Portal.Client.Dialogs.Layer -@using System.Web -@using System.IO -@using System.Text.RegularExpressions -@using System.Net.Http.Headers -@using IoTHub.Portal.Shared.Models.v10.Filters -@using IoTHub.Portal.Client.Enums -@using IoTHub.Portal.Client.Pages.Devices -@using IoTHub.Portal.Client.Validators -@using IoTHub.Portal.Shared.Constants; -@using IoTHub.Portal.Shared.Models -@using IoTHub.Portal.Models -@using IoTHub.Portal.Models.v10.LoRaWAN -@using Microsoft.AspNetCore.WebUtilities - @attribute [Authorize] @inject NavigationManager navigationManager @@ -151,7 +132,7 @@ parameters.Add("planningID", item.Id); parameters.Add("planningName", item.Name); - var result = await DialogService.Show("Confirm Deletion", parameters).Result; + var result = await DialogService.Show("Confirm Deletion", parameters).Result; if (result.Canceled) { diff --git a/src/IoTHub.Portal.Client/_Imports.razor b/src/IoTHub.Portal.Client/_Imports.razor index 5686387fd..0462537ca 100644 --- a/src/IoTHub.Portal.Client/_Imports.razor +++ b/src/IoTHub.Portal.Client/_Imports.razor @@ -9,6 +9,7 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.AspNetCore.WebUtilities @using Microsoft.Extensions.Options diff --git a/src/IoTHub.Portal.Domain/Entities/Planning.cs b/src/IoTHub.Portal.Domain/Entities/Planning.cs index 194900c7d..97c8e1909 100644 --- a/src/IoTHub.Portal.Domain/Entities/Planning.cs +++ b/src/IoTHub.Portal.Domain/Entities/Planning.cs @@ -3,10 +3,6 @@ namespace IoTHub.Portal.Domain.Entities { - using System.Collections.ObjectModel; - using IoTHub.Portal.Domain.Base; - using IoTHub.Portal.Shared.Constants; - public class Planning : EntityBase { /// diff --git a/src/IoTHub.Portal.Domain/Options/DeviceModelImageOptions.cs b/src/IoTHub.Portal.Domain/Options/DeviceModelImageOptions.cs index 941ad3532..eb7081e94 100644 --- a/src/IoTHub.Portal.Domain/Options/DeviceModelImageOptions.cs +++ b/src/IoTHub.Portal.Domain/Options/DeviceModelImageOptions.cs @@ -7,7 +7,7 @@ public class DeviceModelImageOptions { public Uri BaseUri { get; set; } = default!; - public const string ImageContainerName = "device-images-2"; + public const string ImageContainerName = "device-images"; public const string DefaultImageName = "default-template-icon"; diff --git a/src/IoTHub.Portal.Infrastructure/GlobalUsings.cs b/src/IoTHub.Portal.Infrastructure/GlobalUsings.cs index 47bbaa611..bce7db6b3 100644 --- a/src/IoTHub.Portal.Infrastructure/GlobalUsings.cs +++ b/src/IoTHub.Portal.Infrastructure/GlobalUsings.cs @@ -1,11 +1,8 @@ // Copyright (c) CGI France. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -global using System; -global using System.Collections.Generic; +global using System.Collections.ObjectModel; global using System.Globalization; -global using System.IO; -global using System.Linq; global using System.Linq.Dynamic.Core; global using System.Linq.Expressions; global using System.Net; @@ -17,8 +14,6 @@ global using System.Text.Json; global using System.Text.Json.Serialization; global using System.Text.RegularExpressions; -global using System.Threading; -global using System.Threading.Tasks; global using Amazon; global using Amazon.GreengrassV2; global using Amazon.GreengrassV2.Model; @@ -93,4 +88,3 @@ global using Polly.Extensions.Http; global using EntityFramework.Exceptions.PostgreSQL; global using Metrics = Prometheus.Metrics; -global using Stream = System.IO.Stream; diff --git a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs index 44ca1184e..2fb5bd82f 100644 --- a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs +++ b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs @@ -3,20 +3,6 @@ namespace IoTHub.Portal.Infrastructure.Jobs { - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Globalization; - using System.Linq; - using System.Threading.Tasks; - using IoTHub.Portal.Application.Services; - using IoTHub.Portal.Models.v10; - using IoTHub.Portal.Shared.Constants; - using IoTHub.Portal.Shared.Models.v1._0; - using IoTHub.Portal.Shared.Models.v10; - using Microsoft.Extensions.Logging; - using Quartz; - public class PlanningCommand { public string planningId { get; set; } = default!; diff --git a/src/IoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs b/src/IoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs index b4ed9d5d4..52acbd26f 100644 --- a/src/IoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs +++ b/src/IoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs @@ -90,6 +90,9 @@ private static IServiceCollection ConfigureRepositories(this IServiceCollection .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() .AddScoped(); } @@ -99,6 +102,9 @@ private static IServiceCollection ConfigureServices(this IServiceCollection serv .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() .AddTransient(); } } diff --git a/src/IoTHub.Portal.Server/GlobalUsings.cs b/src/IoTHub.Portal.Server/GlobalUsings.cs index a06a17b06..027fb6194 100644 --- a/src/IoTHub.Portal.Server/GlobalUsings.cs +++ b/src/IoTHub.Portal.Server/GlobalUsings.cs @@ -3,7 +3,6 @@ global using System; global using System.Collections.Generic; -global using System.Collections.ObjectModel; global using System.Globalization; global using System.IO; global using System.Linq; @@ -89,9 +88,6 @@ global using UAParser; global using IoTHub.Portal.Application.Mappers; global using System.Text.Json.Serialization; -global using System.Threading; -global using IoTHub.Portal.Client.Shared; global using IoTHub.Portal.Server.Controllers.V10; -global using Microsoft.AspNetCore.Components; global using ValidationException = FluentValidation.ValidationException; global using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute; diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs index cbea9d38f..81a4dd467 100644 --- a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Jobs/SendPlanningCommandJobTests.cs @@ -3,14 +3,6 @@ namespace IoTHub.Portal.Tests.Unit.Infrastructure.Jobs { - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using IoTHub.Portal.Infrastructure.Jobs; - using Microsoft.Extensions.Logging; - using Moq; - using Quartz; - [TestFixture] public class SendPlanningCommandJobTests : BackendUnitTest { From 016a1e333144a91cfc0a6730354f0c64c523eaec Mon Sep 17 00:00:00 2001 From: E068097 Date: Fri, 8 Nov 2024 15:09:01 +0100 Subject: [PATCH 10/17] 2516 add supportLoRaFeatures tag in template file --- src/IoTHub.Portal.Server/Managers/ExportManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/IoTHub.Portal.Server/Managers/ExportManager.cs b/src/IoTHub.Portal.Server/Managers/ExportManager.cs index 98f17ab2c..c4895f403 100644 --- a/src/IoTHub.Portal.Server/Managers/ExportManager.cs +++ b/src/IoTHub.Portal.Server/Managers/ExportManager.cs @@ -74,6 +74,8 @@ public async Task ExportDeviceList(Stream stream) public async Task ExportTemplateFile(Stream stream) { var tags = new List(this.deviceTagService.GetAllTagsNames()); + if (!tags.Contains("supportLoRaFeatures")) + tags.Add("supportLoRaFeatures"); var properties = GetPropertiesToExport(); using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true); From 09710512ae88469bc5edd74ba4d29e0dba469d09 Mon Sep 17 00:00:00 2001 From: E068097 Date: Tue, 12 Nov 2024 14:49:35 +0100 Subject: [PATCH 11/17] #3250 Import device list using the template given --- .../Jobs/SendPlanningCommandJob.cs | 2 +- src/IoTHub.Portal.Server/Managers/ExportManager.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs index 2fb5bd82f..c24b40f49 100644 --- a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs +++ b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs @@ -135,7 +135,7 @@ public void AddNewDevice(DeviceListItem device) { var layer = layers.FirstOrDefault(layer => layer.Id == device.LayerId); - if (layer?.Planning != "None") + if (layer?.Planning is not "None" and not null) { // If the layer linked to a device already has a planning, add the device to the planning list foreach (var planning in this.planningCommands.Where(planning => planning.planningId == layer.Planning)) diff --git a/src/IoTHub.Portal.Server/Managers/ExportManager.cs b/src/IoTHub.Portal.Server/Managers/ExportManager.cs index c4895f403..16c63c4dc 100644 --- a/src/IoTHub.Portal.Server/Managers/ExportManager.cs +++ b/src/IoTHub.Portal.Server/Managers/ExportManager.cs @@ -41,7 +41,7 @@ public async Task ExportDeviceList(Stream stream) using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true); - using var csvWriter = new CsvWriter(writer, CultureInfo.CurrentCulture, leaveOpen: true); + using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture, leaveOpen: true); WriteHeader(tags, properties, csvWriter); @@ -80,7 +80,7 @@ public async Task ExportTemplateFile(Stream stream) using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true); - using var csvWriter = new CsvWriter(writer, CultureInfo.CurrentCulture, leaveOpen: true); + using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture, leaveOpen: true); WriteHeader(tags, properties, csvWriter); From f4dbcc115c35823936962d32aaba2308ecc7ac6c Mon Sep 17 00:00:00 2001 From: E068097 Date: Thu, 14 Nov 2024 16:46:39 +0100 Subject: [PATCH 12/17] #2985 Batch import creates ABP tags in Device Twin for OTAA-based device models --- src/IoTHub.Portal.Server/Managers/ExportManager.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/IoTHub.Portal.Server/Managers/ExportManager.cs b/src/IoTHub.Portal.Server/Managers/ExportManager.cs index 16c63c4dc..267af1165 100644 --- a/src/IoTHub.Portal.Server/Managers/ExportManager.cs +++ b/src/IoTHub.Portal.Server/Managers/ExportManager.cs @@ -290,9 +290,15 @@ private async Task ImportLoRaDevice( TryReadProperty(csvReader, newDevice, c => c.AppKey, string.Empty); TryReadProperty(csvReader, newDevice, c => c.AppEUI, string.Empty); - TryReadProperty(csvReader, newDevice, c => c.AppSKey, string.Empty); - TryReadProperty(csvReader, newDevice, c => c.NwkSKey, string.Empty); - TryReadProperty(csvReader, newDevice, c => c.DevAddr, string.Empty); + if (string.IsNullOrEmpty(newDevice.AppKey) && string.IsNullOrEmpty(newDevice.AppEUI)) + { + // ABP Settings + TryReadProperty(csvReader, newDevice, c => c.AppSKey, string.Empty); + TryReadProperty(csvReader, newDevice, c => c.NwkSKey, string.Empty); + TryReadProperty(csvReader, newDevice, c => c.DevAddr, string.Empty); + newDevice.AppEUI = null; + newDevice.AppKey = null; + } TryReadProperty(csvReader, newDevice, c => c.GatewayID, string.Empty); TryReadProperty(csvReader, newDevice, c => c.Downlink, null); TryReadProperty(csvReader, newDevice, c => c.ClassType, ClassType.A); From fff617e213d950900e6727db7c836cffcea97ddc Mon Sep 17 00:00:00 2001 From: E068097 Date: Thu, 14 Nov 2024 17:06:49 +0100 Subject: [PATCH 13/17] #3251 Import device - data overwritten --- .../Managers/ExportManager.cs | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/IoTHub.Portal.Server/Managers/ExportManager.cs b/src/IoTHub.Portal.Server/Managers/ExportManager.cs index 267af1165..ec05838ff 100644 --- a/src/IoTHub.Portal.Server/Managers/ExportManager.cs +++ b/src/IoTHub.Portal.Server/Managers/ExportManager.cs @@ -99,21 +99,7 @@ private List GetPropertiesToExport() nameof(LoRaDeviceDetails.AppSKey), nameof(LoRaDeviceDetails.NwkSKey), nameof(LoRaDeviceDetails.DevAddr), - nameof(LoRaDeviceDetails.GatewayID), - nameof(LoRaDeviceDetails.Downlink), - nameof(LoRaDeviceDetails.ClassType), - nameof(LoRaDeviceDetails.PreferredWindow), - nameof(LoRaDeviceDetails.Deduplication), - nameof(LoRaDeviceDetails.RX1DROffset), - nameof(LoRaDeviceDetails.RX2DataRate), - nameof(LoRaDeviceDetails.RXDelay), - nameof(LoRaDeviceDetails.ABPRelaxMode), - nameof(LoRaDeviceDetails.SensorDecoder), - nameof(LoRaDeviceDetails.FCntUpStart), - nameof(LoRaDeviceDetails.FCntDownStart), - nameof(LoRaDeviceDetails.FCntResetCounter), - nameof(LoRaDeviceDetails.Supports32BitFCnt), - nameof(LoRaDeviceDetails.KeepAliveTimeout) + nameof(LoRaDeviceDetails.GatewayID) }); } @@ -300,20 +286,6 @@ private async Task ImportLoRaDevice( newDevice.AppKey = null; } TryReadProperty(csvReader, newDevice, c => c.GatewayID, string.Empty); - TryReadProperty(csvReader, newDevice, c => c.Downlink, null); - TryReadProperty(csvReader, newDevice, c => c.ClassType, ClassType.A); - TryReadProperty(csvReader, newDevice, c => c.PreferredWindow, 1); - TryReadProperty(csvReader, newDevice, c => c.Deduplication, DeduplicationMode.Drop); - TryReadProperty(csvReader, newDevice, c => c.RX1DROffset, null); - TryReadProperty(csvReader, newDevice, c => c.RX2DataRate, null); - TryReadProperty(csvReader, newDevice, c => c.RXDelay, null); - TryReadProperty(csvReader, newDevice, c => c.ABPRelaxMode, null); - TryReadProperty(csvReader, newDevice, c => c.SensorDecoder, string.Empty); - TryReadProperty(csvReader, newDevice, c => c.FCntUpStart, null); - TryReadProperty(csvReader, newDevice, c => c.FCntDownStart, null); - TryReadProperty(csvReader, newDevice, c => c.FCntResetCounter, null); - TryReadProperty(csvReader, newDevice, c => c.Supports32BitFCnt, null); - TryReadProperty(csvReader, newDevice, c => c.KeepAliveTimeout, null); _ = await this.loraDeviceService.CheckIfDeviceExists(newDevice.DeviceID) ? await this.loraDeviceService.UpdateDevice(newDevice) From d900c30ae9efe277abb2f0a4d1fd4c83eeab374c Mon Sep 17 00:00:00 2001 From: E068097 Date: Fri, 22 Nov 2024 14:32:01 +0100 Subject: [PATCH 14/17] Unit tests --- .../Components/Planning/EditPlanning.razor | 6 +- .../Planning/DeletePlanningDialog.razor | 1 - .../Startup/AWSServiceCollectionExtension.cs | 17 +- .../Components/Planning/EditPlanningTest.cs | 511 ++++++++++++------ .../Layer/LinkDeviceLayerDialogTest.cs | 177 ++++++ .../Planning/DeletePlanningDialogTest.cs | 58 ++ .../Pages/Planning/PlanningListPageTest.cs | 32 ++ .../DevelopmentConfigHandlerTests.cs | 10 + .../ProductionAWSConfigHandlerTests.cs | 10 + .../ProductionAzureConfigHandlerTests.cs | 10 + .../IoTHub.Portal.Tests.Unit.csproj | 1 - .../Server/Managers/ExportManagerTests.cs | 6 +- 12 files changed, 670 insertions(+), 169 deletions(-) create mode 100644 src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Layer/LinkDeviceLayerDialogTest.cs create mode 100644 src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Planning/DeletePlanningDialogTest.cs diff --git a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor index f52901c8f..a61ff3fe1 100644 --- a/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor +++ b/src/IoTHub.Portal.Client/Components/Planning/EditPlanning.razor @@ -77,7 +77,7 @@ - + @@ -176,7 +176,7 @@ - + @@ -194,7 +194,7 @@ } else { - + } diff --git a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor index 80675b852..39973d46c 100644 --- a/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/Planning/DeletePlanningDialog.razor @@ -1,7 +1,6 @@ @inject ISnackbar Snackbar @inject IPlanningClientService PlanningClientService @inject ILayerClientService LayerClientService -@inject IScheduleClientService ScheduleClientService diff --git a/src/IoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs b/src/IoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs index 93c0d9217..0be446cef 100644 --- a/src/IoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs +++ b/src/IoTHub.Portal.Infrastructure/Startup/AWSServiceCollectionExtension.cs @@ -11,7 +11,8 @@ public static IServiceCollection AddAWSInfrastructureLayer(this IServiceCollecti .ConfigureAWSClient(configuration).Result .ConfigureAWSServices() .ConfigureAWSDeviceModelImages() - .ConfigureAWSSyncJobs(configuration); + .ConfigureAWSSyncJobs(configuration) + .ConfigureAWSSendingCommands(configuration); } private static async Task ConfigureAWSClient(this IServiceCollection services, ConfigHandler configuration) { @@ -110,5 +111,19 @@ private static IServiceCollection ConfigureAWSSyncJobs(this IServiceCollection s }); } + private static IServiceCollection ConfigureAWSSendingCommands(this IServiceCollection services, ConfigHandler configuration) + { + return services.AddQuartz(q => + { + _ = q.AddJob(j => j.WithIdentity(nameof(SendPlanningCommandJob))) + .AddTrigger(t => t + .WithIdentity($"{nameof(SendPlanningCommandJob)}") + .ForJob(nameof(SendPlanningCommandJob)) + .WithSimpleSchedule(s => s + .WithIntervalInMinutes(configuration.SendCommandsToDevicesIntervalInMinutes) + .RepeatForever())); + }); + } + } } diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Components/Planning/EditPlanningTest.cs b/src/IoTHub.Portal.Tests.Unit/Client/Components/Planning/EditPlanningTest.cs index e68f52f24..36149c547 100644 --- a/src/IoTHub.Portal.Tests.Unit/Client/Components/Planning/EditPlanningTest.cs +++ b/src/IoTHub.Portal.Tests.Unit/Client/Components/Planning/EditPlanningTest.cs @@ -3,6 +3,11 @@ namespace IoTHub.Portal.Tests.Unit.Client.Components.Planning { + using Bunit; + using IoTHub.Portal.Client.Dialogs.Planning; + using Moq; + using MudBlazor; + internal class EditPlanningTest : BlazorUnitTest { private Mock mockPlanningClientService; @@ -10,6 +15,8 @@ internal class EditPlanningTest : BlazorUnitTest private Mock mockLayerClientService; private Mock mockDeviceModelsClientService; private Mock mockLoRaWanDeviceModelsClientService; + private Mock mockDialogService; + private FakeNavigationManager mockNavigationManager; public override void Setup() { @@ -20,6 +27,7 @@ public override void Setup() this.mockLayerClientService = MockRepository.Create(); this.mockDeviceModelsClientService = MockRepository.Create(); this.mockLoRaWanDeviceModelsClientService = MockRepository.Create(); + this.mockDialogService = MockRepository.Create(); _ = Services.AddSingleton(this.mockScheduleClientService.Object); _ = Services.AddSingleton(this.mockPlanningClientService.Object); @@ -27,204 +35,208 @@ public override void Setup() _ = Services.AddSingleton(this.mockDeviceModelsClientService.Object); _ = Services.AddSingleton(this.mockLoRaWanDeviceModelsClientService.Object); _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(this.mockDialogService.Object); + + this.mockNavigationManager = Services.GetRequiredService(); } - // TODO: To fix - //[Test] - //public void EditPlanningInit() - //{ - // var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); + [Test] + public void EditPlanningInit() + { + var expectedDeviceModelDto = Fixture.Create(); + var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); - // var planning = new PlanningDto - // { - // DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, - // CommandId = expectedDeviceModelCommandDto[0].Id - // }; - // var firstSchedule = new ScheduleDto - // { - // Start = "00:00" - // }; + var planning = new PlanningDto + { + DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, + CommandId = expectedDeviceModelCommandDto[0].Id + }; + var firstSchedule = new ScheduleDto + { + Start = "00:00" + }; - // var scheduleList = new List - // { - // firstSchedule - // }; + var scheduleList = new List + { + firstSchedule + }; - // _ = this.mockLayerClientService.Setup(service => service.GetLayers()) - // .ReturnsAsync(new List()); + _ = this.mockLayerClientService.Setup(service => service.GetLayers()) + .ReturnsAsync(new List()); - // _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) - // .ReturnsAsync(new PaginationResult - // { - // Items = new List() - // }); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List { expectedDeviceModelDto } + }); - // // Act - // var cut = RenderComponent( - // ComponentParameter.CreateParameter("mode", "New"), - // ComponentParameter.CreateParameter("planning", planning), - // ComponentParameter.CreateParameter("scheduleList", scheduleList ) - // ); + _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) + .ReturnsAsync(expectedDeviceModelCommandDto); - // Assert.AreEqual(DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, cut.Instance.planning.DayOff); - // Assert.AreEqual("00:00", cut.Instance.scheduleList[0].Start); - // cut.WaitForAssertion(() => MockRepository.VerifyAll()); - //} + // Act + var cut = RenderComponent( + ComponentParameter.CreateParameter("mode", "New"), + ComponentParameter.CreateParameter("planning", planning), + ComponentParameter.CreateParameter("scheduleList", scheduleList), + ComponentParameter.CreateParameter("SelectedModel", expectedDeviceModelDto.Name) + ); - // TODO: To fix - //[Test] - //public void EditPlanningInit_AddScheduleShouldNotWork() - //{ - // var expectedLayers = Fixture.CreateMany(1).ToList(); - // var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); + Assert.AreEqual(DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, cut.Instance.planning.DayOff); + Assert.AreEqual("00:00", cut.Instance.scheduleList[0].Start); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } - // var planning = new PlanningDto - // { - // DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, - // CommandId = expectedDeviceModelCommandDto[0].Id - // }; - // var firstSchedule = new ScheduleDto - // { - // Start = "00:00" - // }; + [Test] + public void EditPlanningInit_AddScheduleShouldNotWork() + { + var expectedDeviceModelDto = Fixture.CreateMany(1).ToList(); + var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); - // var scheduleList = new List - // { - // firstSchedule - // }; + var planning = new PlanningDto + { + DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, + CommandId = expectedDeviceModelCommandDto[0].Id + }; + var firstSchedule = new ScheduleDto + { + Start = "00:00" + }; - // _ = this.mockLayerClientService.Setup(service => service.GetLayers()) - // .ReturnsAsync(new List()); + var scheduleList = new List + { + firstSchedule + }; - // _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) - // .ReturnsAsync(new PaginationResult - // { - // Items = expectedLayers - // }); + _ = this.mockLayerClientService.Setup(service => service.GetLayers()) + .ReturnsAsync(new List()); - // _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) - // .ReturnsAsync(expectedDeviceModelCommandDto); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = expectedDeviceModelDto + }); - // // Act - // var cut = RenderComponent( - // ComponentParameter.CreateParameter("mode", "New"), - // ComponentParameter.CreateParameter("planning", planning), - // ComponentParameter.CreateParameter("scheduleList", scheduleList ) - // ); + _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) + .ReturnsAsync(expectedDeviceModelCommandDto); - // var editPlanningAddLayers = cut.WaitForElement("#editPlanningAddLayers"); - // editPlanningAddLayers.Click(); + // Act + var cut = RenderComponent( + ComponentParameter.CreateParameter("mode", "New"), + ComponentParameter.CreateParameter("planning", planning), + ComponentParameter.CreateParameter("scheduleList", scheduleList ) + ); - // Assert.AreEqual(1, cut.Instance.scheduleList.Count); - // cut.WaitForAssertion(() => MockRepository.VerifyAll()); - //} + var editPlanningAddSchedule = cut.WaitForElement("#addScheduleButton"); + editPlanningAddSchedule.Click(); - // TODO: To fix - //[Test] - //public async Task EditPlanningInit_AddSchedule() - //{ - // var expectedLayers = Fixture.CreateMany(1).ToList(); - // var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); + Assert.AreEqual(1, cut.Instance.scheduleList.Count); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } - // var planning = new PlanningDto - // { - // DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, - // CommandId = expectedDeviceModelCommandDto[0].Id - // }; - // var firstSchedule = new ScheduleDto - // { - // Start = "00:00", - // CommandId = expectedDeviceModelCommandDto[0].Id - // }; + [Test] + public async Task EditPlanningInit_AddSchedule() + { + var expectedDeviceModelDto = Fixture.CreateMany(1).ToList(); + var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); - // var scheduleList = new List - // { - // firstSchedule - // }; + var planning = new PlanningDto + { + DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, + CommandId = expectedDeviceModelCommandDto[0].Id + }; + var firstSchedule = new ScheduleDto + { + Start = "00:00", + CommandId = expectedDeviceModelCommandDto[0].Id + }; - // _ = this.mockLayerClientService.Setup(service => service.GetLayers()) - // .ReturnsAsync(new List()); + var scheduleList = new List + { + firstSchedule + }; - // _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) - // .ReturnsAsync(new PaginationResult - // { - // Items = expectedLayers - // }); + _ = this.mockLayerClientService.Setup(service => service.GetLayers()) + .ReturnsAsync(new List()); - // _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) - // .ReturnsAsync(expectedDeviceModelCommandDto); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = expectedDeviceModelDto + }); - // // Act - // var cut = RenderComponent( - // ComponentParameter.CreateParameter("mode", "New"), - // ComponentParameter.CreateParameter("planning", planning), - // ComponentParameter.CreateParameter("scheduleList", scheduleList ) - // ); + _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) + .ReturnsAsync(expectedDeviceModelCommandDto); - // var endField = cut.FindComponents>()[2]; - // await cut.InvokeAsync(() => endField.Instance.SetText("23:59")); + // Act + var cut = RenderComponent( + ComponentParameter.CreateParameter("mode", "New"), + ComponentParameter.CreateParameter("planning", planning), + ComponentParameter.CreateParameter("scheduleList", scheduleList ) + ); - // var editPlanningAddLayers = cut.WaitForElement("#editPlanningAddLayers"); - // editPlanningAddLayers.Click(); + var endField = cut.FindComponents>()[2]; + await cut.InvokeAsync(() => endField.Instance.SetText("23:59")); - // Assert.AreEqual(2, cut.Instance.scheduleList.Count); - // cut.WaitForAssertion(() => MockRepository.VerifyAll()); - //} + var editPlanningAddSchedule = cut.WaitForElement("#addScheduleButton"); + editPlanningAddSchedule.Click(); - // TODO: To fix - //[Test] - //public async Task EditPlanningInit_DeleteSchedule() - //{ - // var expectedLayers = Fixture.CreateMany(1).ToList(); - // var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); + Assert.AreEqual(2, cut.Instance.scheduleList.Count); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } - // var planning = new PlanningDto - // { - // DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, - // CommandId = expectedDeviceModelCommandDto[0].Id - // }; - // var firstSchedule = new ScheduleDto - // { - // Start = "00:00", - // CommandId = expectedDeviceModelCommandDto[0].Id - // }; + [Test] + public async Task EditPlanningInit_DeleteSchedule() + { + var expectedDeviceModelDto = Fixture.CreateMany(1).ToList(); + var expectedDeviceModelCommandDto = Fixture.CreateMany(3).ToList(); - // var scheduleList = new List - // { - // firstSchedule - // }; + var planning = new PlanningDto + { + DayOff = DaysEnumFlag.DaysOfWeek.Saturday | DaysEnumFlag.DaysOfWeek.Sunday, + CommandId = expectedDeviceModelCommandDto[0].Id + }; + var firstSchedule = new ScheduleDto + { + Start = "00:00", + CommandId = expectedDeviceModelCommandDto[0].Id + }; - // _ = this.mockLayerClientService.Setup(service => service.GetLayers()) - // .ReturnsAsync(new List()); + var scheduleList = new List + { + firstSchedule + }; - // _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels(It.IsAny())) - // .ReturnsAsync(new PaginationResult - // { - // Items = expectedLayers - // }); + _ = this.mockLayerClientService.Setup(service => service.GetLayers()) + .ReturnsAsync(new List()); - // _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) - // .ReturnsAsync(expectedDeviceModelCommandDto); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = expectedDeviceModelDto + }); - // // Act - // var cut = RenderComponent( - // ComponentParameter.CreateParameter("mode", "New"), - // ComponentParameter.CreateParameter("planning", planning), - // ComponentParameter.CreateParameter("scheduleList", scheduleList ) - // ); + _ = this.mockLoRaWanDeviceModelsClientService.Setup(service => service.GetDeviceModelCommands(It.IsAny())) + .ReturnsAsync(expectedDeviceModelCommandDto); + + // Act + var cut = RenderComponent( + ComponentParameter.CreateParameter("mode", "New"), + ComponentParameter.CreateParameter("planning", planning), + ComponentParameter.CreateParameter("scheduleList", scheduleList ) + ); - // var endField = cut.FindComponents>()[2]; - // await cut.InvokeAsync(() => endField.Instance.SetText("23:59")); + var endField = cut.FindComponents>()[2]; + await cut.InvokeAsync(() => endField.Instance.SetText("23:59")); - // var editPlanningAddLayers = cut.WaitForElement("#editPlanningAddLayers"); - // editPlanningAddLayers.Click(); + var editPlanningAddSchedule = cut.WaitForElement("#addScheduleButton"); + editPlanningAddSchedule.Click(); - // var editPlanningDeleteLayers = cut.FindAll("#editPlanningDeleteLayers")[1]; - // editPlanningDeleteLayers.Click(); + var editPlanningDeleteSchedule = cut.FindAll("#deleteScheduleButton")[1]; + editPlanningDeleteSchedule.Click(); - // Assert.AreEqual(1, cut.Instance.scheduleList.Count); - // cut.WaitForAssertion(() => MockRepository.VerifyAll()); - //} + Assert.AreEqual(1, cut.Instance.scheduleList.Count); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } [Test] public void EditPlanningInit_ProblemDetailsException() @@ -479,5 +491,184 @@ public void EditPlanningInit_ChangeOffDay() // cut.WaitForAssertion(() => MockRepository.VerifyAll()); //} + + [Test] + public void ClickOnDeleteShouldDisplayConfirmationDialogAndReturnIfAborted() + { + var mockPlanning = new PlanningDto + { + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + var mockDeviceModel = new DeviceModelDto + { + ModelId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + var mockScheduleList = Fixture.CreateMany(2).ToList(); + + _ = this.mockLayerClientService.Setup(service => + service.GetLayers()) + .ReturnsAsync(new List + { + new LayerDto { Id = Guid.NewGuid().ToString(), Name = Guid.NewGuid().ToString() } + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Cancel); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(parameters => parameters.Add(p => p.mode, "Edit") + .Add(p => p.planning, mockPlanning) + .Add(p => p.scheduleList, mockScheduleList) + .Add(p => p.initScheduleList, new List(mockScheduleList)) + .Add(p => p.SelectedModel, mockDeviceModel.Name)); + + var deleteButton = cut.WaitForElement("#deleteButton"); + deleteButton.Click(); + + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + + [Test] + public void ClickOnDeleteShouldDisplayConfirmationDialogAndRedirectIfConfirmed() + { + var mockPlanning = new PlanningDto + { + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + var mockDeviceModel = new List + { + new DeviceModelDto + { + ModelId = Guid.NewGuid().ToString(), Name = Guid.NewGuid().ToString() + }, + new DeviceModelDto + { + ModelId = Guid.NewGuid().ToString(), Name = Guid.NewGuid().ToString() + } + }; + + var mockScheduleList = Fixture.CreateMany(2).ToList(); + + var deviceModels = Fixture.CreateMany(2).ToList(); + + var expectedPaginatedDeviceModels = new PaginationResult() + { + Items = mockDeviceModel.ToList(), + TotalItems = mockDeviceModel.Count + }; + + _ = this.mockLayerClientService.Setup(service => + service.GetLayers()) + .ReturnsAsync(new List + { + new LayerDto { Id = Guid.NewGuid().ToString(), Name = Guid.NewGuid().ToString() } + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(parameters => parameters.Add(p => p.mode, "Edit") + .Add(p => p.planning, mockPlanning) + .Add(p => p.scheduleList, mockScheduleList) + .Add(p => p.initScheduleList, new List(mockScheduleList)) + .Add(p => p.SelectedModel, mockDeviceModel[0].Name)); + + var deleteButton = cut.WaitForElement("#deleteButton"); + deleteButton.Click(); + + cut.WaitForState(() => this.mockNavigationManager.Uri.EndsWith("/planning", StringComparison.OrdinalIgnoreCase)); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task DisplayLayersRenderCorrectly() + { + var mockPlanning = new PlanningDto + { + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + var mockDeviceModel = new DeviceModelDto + { + ModelId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + var mockScheduleList = Fixture.CreateMany(2).ToList(); + + var mockPrincipalLayer = new LayerDto + { + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Planning = null, + Father = null + }; + + var mockSubLayer1 = new LayerDto + { + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Planning = mockPlanning.Id, + Father = mockPrincipalLayer.Id + }; + + var mockSubLayer2 = new LayerDto + { + Id = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString(), + Planning = Guid.NewGuid().ToString(), + Father = mockPrincipalLayer.Id + }; + + _ = this.mockLayerClientService.Setup(service => + service.GetLayers()) + .ReturnsAsync(new List + { + mockPrincipalLayer, + mockSubLayer1, + mockSubLayer2 + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + + var cut = RenderComponent(parameters => parameters.Add(p => p.mode, "Edit") + .Add(p => p.planning, mockPlanning) + .Add(p => p.scheduleList, mockScheduleList) + .Add(p => p.initScheduleList, new List(mockScheduleList)) + .Add(p => p.SelectedModel, mockDeviceModel.Name)); + + var tooltips = cut.FindComponents(); + _ = tooltips[0].Instance.Text.Should().Be("Add layer"); + _ = tooltips[1].Instance.Text.Should().Be("Already registered"); + _ = tooltips[2].Instance.Text.Should().Be("Registered on other planning"); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } } } diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Layer/LinkDeviceLayerDialogTest.cs b/src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Layer/LinkDeviceLayerDialogTest.cs new file mode 100644 index 000000000..eb3ba89a3 --- /dev/null +++ b/src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Layer/LinkDeviceLayerDialogTest.cs @@ -0,0 +1,177 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace IoTHub.Portal.Tests.Unit.Client.Dialogs.Layer +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using IoTHub.Portal.Client.Dialogs.Layer; + + public class LinkDeviceLayerDialogTest : BlazorUnitTest + { + private Mock mockDeviceClientService; + private Mock mockLayerClientService; + private Mock mockDeviceModelsClientService; + + private readonly string apiBaseUrl = "api/devices"; + + public override void Setup() + { + base.Setup(); + + this.mockDeviceClientService = MockRepository.Create(); + this.mockLayerClientService = MockRepository.Create(); + this.mockDeviceModelsClientService = MockRepository.Create(); + + _ = Services.AddSingleton(this.mockDeviceClientService.Object); + _ = Services.AddSingleton(this.mockLayerClientService.Object); + _ = Services.AddSingleton(this.mockDeviceModelsClientService.Object); + } + + [Test] + public async Task LinkDeviceLayerDialog_Search_RendersCorrectlyAsync() + { + // Arrange + var searchedDevices = Fixture.CreateMany>(3).ToList(); + var expectedLayerDto = Fixture.Create(); + + _ = this.mockDeviceClientService.Setup(service => + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=5&searchText=")) + .ReturnsAsync(new PaginationResult + { + Items = new[] { new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true }, + new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true }} + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List() + }); + + // Act + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"InitLayer", expectedLayerDto}, + {"LayerList", new HashSet()} + }; + + _ = await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); + + // Assert + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(2)); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task LinkDeviceLayerDialog_Search_ShouldDisplayDevicesAsync() + { + // Arrange + var searchedDevices = Fixture.CreateMany>(3).ToList(); + var expectedLayerDto = Fixture.Create(); + + var mockDeviceModel = new DeviceModelDto + { + ModelId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + + _ = this.mockDeviceClientService.Setup(service => + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=5&searchText=")) + .ReturnsAsync(new PaginationResult + { + Items = new[] { new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true, DeviceModelId = mockDeviceModel.ModelId }, + new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true, DeviceModelId = Guid.NewGuid().ToString() }} + }); + + _ = this.mockDeviceClientService.Setup(service => + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=5&searchText=&modelId={mockDeviceModel.ModelId}")) + .ReturnsAsync(new PaginationResult + { + Items = new[] { new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true, DeviceModelId = mockDeviceModel.ModelId } } + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + mockDeviceModel + } + }); + + // Act + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"InitLayer", expectedLayerDto}, + {"LayerList", new HashSet()} + }; + + _ = await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); + cut.WaitForElement("#saveButton").Click(); + + // Assert + cut.WaitForAssertion(() => cut.FindAll("table tbody tr").Count.Should().Be(1)); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public async Task LinkDeviceLayerDialog_Save_UpdatesDevices() + { + // Arrange + var searchedDevices = Fixture.CreateMany>(3).ToList(); + var expectedLayerDto = Fixture.Create(); + + var mockDeviceModel = new DeviceModelDto + { + ModelId = Guid.NewGuid().ToString(), + Name = Guid.NewGuid().ToString() + }; + + + _ = this.mockDeviceClientService.Setup(service => + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=5&searchText=")) + .ReturnsAsync(new PaginationResult + { + Items = new[] { new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true, DeviceModelId = mockDeviceModel.ModelId }, + new DeviceListItem { DeviceID = Guid.NewGuid().ToString(), IsEnabled = true, IsConnected = true, DeviceModelId = Guid.NewGuid().ToString() }} + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModelsAsync(It.IsAny())) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + mockDeviceModel + } + }); + + _ = this.mockLayerClientService.Setup(service => service.UpdateLayer(expectedLayerDto)) + .Returns(Task.CompletedTask); + + // Act + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"InitLayer", expectedLayerDto}, + {"LayerList", new HashSet()} + }; + + _ = await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); + cut.WaitForElement("#save").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + } +} diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Planning/DeletePlanningDialogTest.cs b/src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Planning/DeletePlanningDialogTest.cs new file mode 100644 index 000000000..cd54c36f3 --- /dev/null +++ b/src/IoTHub.Portal.Tests.Unit/Client/Dialogs/Planning/DeletePlanningDialogTest.cs @@ -0,0 +1,58 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace IoTHub.Portal.Tests.Unit.Client.Dialogs.Planning +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using IoTHub.Portal.Client.Dialogs.Planning; + + [TestFixture] + public class DeletePlanningDialogTest : BlazorUnitTest + { + private Mock mockPlanningClientService; + private Mock mockLayerClientService; + + public override void Setup() + { + base.Setup(); + + this.mockPlanningClientService = MockRepository.Create(); + this.mockLayerClientService = MockRepository.Create(); + + _ = Services.AddSingleton(this.mockPlanningClientService.Object); + _ = Services.AddSingleton(this.mockLayerClientService.Object); + } + + [Test] + public async Task DeletePlanning_PlanningDeleted() + { + // Arrange + var planningId = Guid.NewGuid().ToString(); + var planningName = Guid.NewGuid().ToString(); + + _ = this.mockLayerClientService.Setup(service => service.GetLayers()) + .ReturnsAsync(new List()); + + _ = this.mockPlanningClientService.Setup(service => service.DeletePlanning(planningId)) + .Returns(Task.CompletedTask); + + var cut = RenderComponent(); + var service = Services.GetService() as DialogService; + + var parameters = new DialogParameters + { + {"planningID", planningId}, + {"planningName", planningName} + }; + + // Act + _ = await cut.InvokeAsync(() => service?.Show(string.Empty, parameters)); + cut.WaitForElement("#delete-planning").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + } +} diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Pages/Planning/PlanningListPageTest.cs b/src/IoTHub.Portal.Tests.Unit/Client/Pages/Planning/PlanningListPageTest.cs index 5e4dae410..7bb4d644b 100644 --- a/src/IoTHub.Portal.Tests.Unit/Client/Pages/Planning/PlanningListPageTest.cs +++ b/src/IoTHub.Portal.Tests.Unit/Client/Pages/Planning/PlanningListPageTest.cs @@ -3,19 +3,24 @@ namespace IoTHub.Portal.Tests.Unit.Client.Pages.Planning { + using IoTHub.Portal.Client.Dialogs.Planning; + [TestFixture] internal class PlanningListPageTest : BlazorUnitTest { private Mock mockPlanningClientService; private FakeNavigationManager mockNavigationManager; + private Mock mockDialogService; public override void Setup() { base.Setup(); this.mockPlanningClientService = MockRepository.Create(); + this.mockDialogService = MockRepository.Create(); _ = Services.AddSingleton(this.mockPlanningClientService.Object); _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); + _ = Services.AddSingleton(this.mockDialogService.Object); this.mockNavigationManager = Services.GetRequiredService(); } @@ -90,5 +95,32 @@ public void OnInitializedAsync_ListDetailPlanningPage() cut.WaitForAssertion(() => this.mockNavigationManager.Uri.Should().EndWith($"/planning/{expectedPlannings[0].Id}")); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } + + [Test] + public void DeletePlanning_Clicked_DeletePlanningDialogIsShown() + { + // Arrange + var planningId = Guid.NewGuid().ToString(); + + _ = this.mockPlanningClientService.Setup(service => + service.GetPlannings()).ReturnsAsync(new List + { + new PlanningDto() { Id = planningId, Name = Guid.NewGuid().ToString() } + }); + + var mockDialogReference = MockRepository.Create(); + _ = mockDialogReference.Setup(c => c.Result).ReturnsAsync(DialogResult.Ok("Ok")); + _ = this.mockDialogService.Setup(c => c.Show(It.IsAny(), It.IsAny())) + .Returns(mockDialogReference.Object); + + var cut = RenderComponent(); + cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); + + // Act + cut.WaitForElement($"#delete-planning-{planningId}").Click(); + + // Assert + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } } } diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs index 337702b63..b8de5c89d 100644 --- a/src/IoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/DevelopmentConfigHandlerTests.cs @@ -299,5 +299,15 @@ public void DbProviderKeyShouldBeExpectedDefaultValue() // Assert _ = developmentConfigHandler.DbProvider.Should().Be(DbProviders.PostgreSQL); } + + [Test] + public void SendCommandsToDevicesIntervalInMinutesConfigMustHaveDefaultValue() + { + // Arrange + var developmentConfigHandler = new DevelopmentConfigHandler(new ConfigurationManager()); + + // Assert + _ = developmentConfigHandler.SendCommandsToDevicesIntervalInMinutes.Should().Be(10); + } } } diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs index 520ab0732..c276523ae 100644 --- a/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAWSConfigHandlerTests.cs @@ -314,5 +314,15 @@ public void DbProviderKeyShouldBeExpectedDefaultValue() // Assert _ = productionAWSConfigHandler.DbProvider.Should().Be(DbProviders.PostgreSQL); } + + [Test] + public void SendCommandsToDevicesIntervalInMinutesConfigMustHaveDefaultValue() + { + // Arrange + var productionAWSConfigHandler = new ProductionAWSConfigHandler(new ConfigurationManager()); + + // Assert + _ = productionAWSConfigHandler.SendCommandsToDevicesIntervalInMinutes.Should().Be(10); + } } } diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs index e575c2bce..b97e4216d 100644 --- a/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/ProductionAzureConfigHandlerTests.cs @@ -300,5 +300,15 @@ public void DbProviderKeyShouldBeExpectedDefaultValue() // Assert _ = productionConfigHandler.DbProvider.Should().Be(DbProviders.PostgreSQL); } + + [Test] + public void SendCommandsToDevicesIntervalInMinutesConfigMustHaveDefaultValue() + { + // Arrange + var productionConfigHandler = new ProductionAzureConfigHandler(new ConfigurationManager()); + + // Assert + _ = productionConfigHandler.SendCommandsToDevicesIntervalInMinutes.Should().Be(10); + } } } diff --git a/src/IoTHub.Portal.Tests.Unit/IoTHub.Portal.Tests.Unit.csproj b/src/IoTHub.Portal.Tests.Unit/IoTHub.Portal.Tests.Unit.csproj index e2c5ce67f..7b019d5ca 100644 --- a/src/IoTHub.Portal.Tests.Unit/IoTHub.Portal.Tests.Unit.csproj +++ b/src/IoTHub.Portal.Tests.Unit/IoTHub.Portal.Tests.Unit.csproj @@ -46,7 +46,6 @@ - diff --git a/src/IoTHub.Portal.Tests.Unit/Server/Managers/ExportManagerTests.cs b/src/IoTHub.Portal.Tests.Unit/Server/Managers/ExportManagerTests.cs index 662d484ee..b7dfb9de2 100644 --- a/src/IoTHub.Portal.Tests.Unit/Server/Managers/ExportManagerTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Server/Managers/ExportManagerTests.cs @@ -117,7 +117,7 @@ public void ExportDeviceListLoRaEnabledShouldWriteStreamAndDisplayLoRaSpecificFi using var reader = new StreamReader(fileStream); var header = reader.ReadLine(); - _ = header.Split(",").Length.Should().Be(28); + _ = header.Split(",").Length.Should().Be(14); var content = reader.ReadToEnd(); _ = content.TrimEnd().Split("\r\n").Length.Should().Be(2); @@ -151,7 +151,7 @@ public void ExportTemplateFileLoRaDisabledShouldWriteStream() using var reader = new StreamReader(fileStream); var content = reader.ReadToEnd(); _ = content.TrimEnd().Split("\r\n").Length.Should().Be(1); - _ = content.Split(",").Length.Should().Be(7); + _ = content.Split(",").Length.Should().Be(8); } [Test] @@ -181,7 +181,7 @@ public void ExportTemplateFileLoRaEnabledShouldWriteStreamAndDisplayLoRaSpecific using var reader = new StreamReader(fileStream); var content = reader.ReadToEnd(); _ = content.TrimEnd().Split("\r\n").Length.Should().Be(1); - _ = content.Split(",").Length.Should().Be(27); + _ = content.Split(",").Length.Should().Be(14); MockRepository.VerifyAll(); } From 932921d947b783f6095b07285ab0240724ebff9c Mon Sep 17 00:00:00 2001 From: judramos Date: Mon, 25 Nov 2024 09:02:15 +0100 Subject: [PATCH 15/17] Update src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs Co-authored-by: Kevin BEAUGRAND <9513635+kbeaugrand@users.noreply.github.com> --- src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs index c24b40f49..c90e22ab0 100644 --- a/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs +++ b/src/IoTHub.Portal.Infrastructure/Jobs/SendPlanningCommandJob.cs @@ -135,7 +135,7 @@ public void AddNewDevice(DeviceListItem device) { var layer = layers.FirstOrDefault(layer => layer.Id == device.LayerId); - if (layer?.Planning is not "None" and not null) + if (layer?.Planning is not null and not "None") { // If the layer linked to a device already has a planning, add the device to the planning list foreach (var planning in this.planningCommands.Where(planning => planning.planningId == layer.Planning)) From a4c778a27ef5d466fa1069e5e3d8b454014ab4ab Mon Sep 17 00:00:00 2001 From: E068097 Date: Fri, 29 Nov 2024 11:03:45 +0100 Subject: [PATCH 16/17] #2958 Remove 'Connection State' and 'Last status update' columns --- .../Concentrators/ConcentratorSearch.razor | 14 - .../Dialogs/Layer/LinkDeviceLayerDialog.razor | 1 + .../Pages/Devices/DeviceListPage.razor | 33 +- .../Pages/Layer/LayerListPage.razor | 1 + .../Concentrator/ConcentratorListPage.razor | 18 +- src/IoTHub.Portal.Domain/Entities/Device.cs | 5 + src/IoTHub.Portal.Domain/Entities/IDevice.cs | 5 + .../Mappers/DeviceTwinMapper.cs | 2 + .../Mappers/LoRaDeviceMapper.cs | 2 + .../Services/DeviceServiceBase.cs | 1 + ... cascade plannings & schedules.Designer.cs | 712 +++++++++++++++++ ...18_Delete cascade plannings & schedules.cs | 60 ++ ...128160016_Add LastActivityTime.Designer.cs | 715 ++++++++++++++++++ .../20241128160016_Add LastActivityTime.cs | 30 + .../PortalDbContextModelSnapshot.cs | 31 +- ...128155624_Add LastActivityTime.Designer.cs | 715 ++++++++++++++++++ .../20241128155624_Add LastActivityTime.cs | 30 + .../PortalDbContextModelSnapshot.cs | 3 + .../Models/v1.0/DeviceDetails.cs | 5 + .../Models/v1.0/DeviceListItem.cs | 5 + .../Models/v1.0/IDeviceDetails.cs | 5 + .../Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs | 5 + .../Concentrators/ConcentratorSearchTests.cs | 2 - .../Pages/Devices/DevicesListPageTests.cs | 3 - .../Mappers/DeviceTwinMapperTests.cs | 4 + 25 files changed, 2337 insertions(+), 70 deletions(-) create mode 100644 src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.Designer.cs create mode 100644 src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.cs create mode 100644 src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.Designer.cs create mode 100644 src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.cs create mode 100644 src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.Designer.cs create mode 100644 src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.cs diff --git a/src/IoTHub.Portal.Client/Components/Concentrators/ConcentratorSearch.razor b/src/IoTHub.Portal.Client/Components/Concentrators/ConcentratorSearch.razor index 633db9ded..c794db00b 100644 --- a/src/IoTHub.Portal.Client/Components/Concentrators/ConcentratorSearch.razor +++ b/src/IoTHub.Portal.Client/Components/Concentrators/ConcentratorSearch.razor @@ -20,20 +20,6 @@ - - Connection state - - - Connected - - - Disconnected - - - All - - - diff --git a/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor b/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor index 09279b12f..acd8f6e23 100644 --- a/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/Layer/LinkDeviceLayerDialog.razor @@ -228,6 +228,7 @@ deviceDetails.IsConnected = device.IsConnected; deviceDetails.IsEnabled = device.IsEnabled; deviceDetails.StatusUpdatedTime = device.StatusUpdatedTime; + deviceDetails.LastActivityTime = device.LastActivityTime; deviceDetails.Labels = device.Labels.ToList(); deviceDetails.LayerId = device.LayerId; diff --git a/src/IoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor b/src/IoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor index 26ff988e2..f1eef1085 100644 --- a/src/IoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor +++ b/src/IoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor @@ -80,20 +80,6 @@ - - Connection state - - - Connected - - - Disconnected - - - All - - - @@ -161,8 +147,7 @@ Device Allowed - Connection state - Last status update + Last activity time @if (Portal.IsLoRaSupported) { Telemetry @@ -199,21 +184,7 @@ } - - @if (context.IsConnected) - { - - - - } - else - { - - - - } - - @context.StatusUpdatedTime + @context.LastActivityTime @if (Portal.IsLoRaSupported) { diff --git a/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor b/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor index 72b6ff743..ac58f5f4d 100644 --- a/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor +++ b/src/IoTHub.Portal.Client/Pages/Layer/LayerListPage.razor @@ -150,6 +150,7 @@ deviceDetails.IsConnected = device.IsConnected; deviceDetails.IsEnabled = device.IsEnabled; deviceDetails.StatusUpdatedTime = device.StatusUpdatedTime; + deviceDetails.LastActivityTime = device.LastActivityTime; deviceDetails.Labels = device.Labels.ToList(); deviceDetails.LayerId = device.LayerId; diff --git a/src/IoTHub.Portal.Client/Pages/LoRaWAN/Concentrator/ConcentratorListPage.razor b/src/IoTHub.Portal.Client/Pages/LoRaWAN/Concentrator/ConcentratorListPage.razor index 7d3306801..115a95125 100644 --- a/src/IoTHub.Portal.Client/Pages/LoRaWAN/Concentrator/ConcentratorListPage.razor +++ b/src/IoTHub.Portal.Client/Pages/LoRaWAN/Concentrator/ConcentratorListPage.razor @@ -12,8 +12,7 @@ - - + @@ -31,7 +30,6 @@ Device Allowed - Connection state See details Delete @@ -54,20 +52,6 @@ } - - @if (context.IsConnected) - { - - - - } - else - { - - - - } - diff --git a/src/IoTHub.Portal.Domain/Entities/Device.cs b/src/IoTHub.Portal.Domain/Entities/Device.cs index 0e4b0baf5..b5eea996b 100644 --- a/src/IoTHub.Portal.Domain/Entities/Device.cs +++ b/src/IoTHub.Portal.Domain/Entities/Device.cs @@ -30,6 +30,11 @@ public class Device : EntityBase /// public DateTime StatusUpdatedTime { get; set; } + /// + /// Gets or sets the last activity time. + /// + public DateTime LastActivityTime { get; set; } + /// /// The current version of the device stored n he database /// diff --git a/src/IoTHub.Portal.Domain/Entities/IDevice.cs b/src/IoTHub.Portal.Domain/Entities/IDevice.cs index f57fafc18..9ac90dde3 100644 --- a/src/IoTHub.Portal.Domain/Entities/IDevice.cs +++ b/src/IoTHub.Portal.Domain/Entities/IDevice.cs @@ -35,6 +35,11 @@ public interface IDevice /// public DateTime StatusUpdatedTime { get; set; } + /// + /// Gets or sets the last activity time. + /// + public DateTime LastActivityTime { get; set; } + /// /// The device labels. /// diff --git a/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs b/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs index 616b53d7b..ffac2aa97 100644 --- a/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs +++ b/src/IoTHub.Portal.Infrastructure/Mappers/DeviceTwinMapper.cs @@ -36,6 +36,7 @@ public DeviceDetails CreateDeviceDetails(Twin twin, IEnumerable tags) IsConnected = twin.ConnectionState == DeviceConnectionState.Connected, IsEnabled = twin.Status == DeviceStatus.Enabled, StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, + LastActivityTime = twin.LastActivityTime ?? DateTime.MinValue, LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(DeviceDetails.LayerId)) }; @@ -57,6 +58,7 @@ public DeviceListItem CreateDeviceListItem(Twin twin) IsConnected = twin.ConnectionState == DeviceConnectionState.Connected, IsEnabled = twin.Status == DeviceStatus.Enabled, StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, + LastActivityTime = twin.LastActivityTime ?? DateTime.MinValue, DeviceName = DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.DeviceName)), Image = this.deviceModelImageManager.GetDeviceModelImageAsync(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceDetails.ModelId))!).Result, SupportLoRaFeatures = bool.Parse(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.SupportLoRaFeatures)) ?? "false"), diff --git a/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs b/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs index 16d1c000f..85886cc0e 100644 --- a/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs +++ b/src/IoTHub.Portal.Infrastructure/Mappers/LoRaDeviceMapper.cs @@ -35,6 +35,7 @@ public LoRaDeviceDetails CreateDeviceDetails(Twin twin, IEnumerable tags IsConnected = twin.ConnectionState == DeviceConnectionState.Connected, IsEnabled = twin.Status == DeviceStatus.Enabled, StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, + LastActivityTime = twin.LastActivityTime ?? DateTime.MinValue, LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(LoRaDeviceDetails.LayerId)), GatewayID = DeviceHelper.RetrieveDesiredPropertyValue(twin, nameof(LoRaDeviceDetails.GatewayID)), SensorDecoder = @@ -150,6 +151,7 @@ public DeviceListItem CreateDeviceListItem(Twin twin) IsEnabled = twin.Status == DeviceStatus.Enabled, LayerId = DeviceHelper.RetrieveTagValue(twin, nameof(LoRaDeviceDetails.LayerId)), StatusUpdatedTime = twin.StatusUpdatedTime ?? DateTime.MinValue, + LastActivityTime = twin.LastActivityTime ?? DateTime.MinValue, SupportLoRaFeatures = bool.Parse(DeviceHelper.RetrieveTagValue(twin, nameof(DeviceListItem.SupportLoRaFeatures)) ?? "false") diff --git a/src/IoTHub.Portal.Infrastructure/Services/DeviceServiceBase.cs b/src/IoTHub.Portal.Infrastructure/Services/DeviceServiceBase.cs index 77ef6bd34..fa1b126cf 100644 --- a/src/IoTHub.Portal.Infrastructure/Services/DeviceServiceBase.cs +++ b/src/IoTHub.Portal.Infrastructure/Services/DeviceServiceBase.cs @@ -104,6 +104,7 @@ public async Task> GetDevices(string searchText IsEnabled = device.IsEnabled, IsConnected = device.IsConnected, StatusUpdatedTime = device.StatusUpdatedTime, + LastActivityTime = device.LastActivityTime, DeviceModelId = device.DeviceModelId, SupportLoRaFeatures = device is LorawanDevice, HasLoRaTelemetry = device is LorawanDevice && ((LorawanDevice) device).Telemetry.Any(), diff --git a/src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.Designer.cs b/src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.Designer.cs new file mode 100644 index 000000000..06ddd1317 --- /dev/null +++ b/src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.Designer.cs @@ -0,0 +1,712 @@ +// +using System; +using IoTHub.Portal.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IoTHub.Portal.MySql.Migrations +{ + [DbContext(typeof(PortalDbContext))] + [Migration("20241128154918_Delete cascade plannings & schedules")] + partial class Deletecascadeplanningsschedules + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Concentrator", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ClientThumbprint") + .HasColumnType("longtext"); + + b.Property("DeviceType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsConnected") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LoraRegion") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Concentrators"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsConnected") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LayerId") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StatusUpdatedTime") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("Devices", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ABPRelaxMode") + .HasColumnType("tinyint(1)"); + + b.Property("AppEUI") + .HasColumnType("longtext"); + + b.Property("ClassType") + .HasColumnType("int"); + + b.Property("Deduplication") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Downlink") + .HasColumnType("tinyint(1)"); + + b.Property("IsBuiltin") + .HasColumnType("tinyint(1)"); + + b.Property("KeepAliveTimeout") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PreferredWindow") + .HasColumnType("int"); + + b.Property("RXDelay") + .HasColumnType("int"); + + b.Property("SensorDecoder") + .HasColumnType("longtext"); + + b.Property("SupportLoRaFeatures") + .HasColumnType("tinyint(1)"); + + b.Property("UseOTAA") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("DeviceModels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Confirmed") + .HasColumnType("tinyint(1)"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Frame") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsBuiltin") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModelProperty", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsWritable") + .HasColumnType("tinyint(1)"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PropertyType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelProperties"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTag", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Required") + .HasColumnType("tinyint(1)"); + + b.Property("Searchable") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("DeviceTags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("DeviceId") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceId") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("EdgeDeviceId"); + + b.ToTable("DeviceTagValues"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConnectionState") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NbDevices") + .HasColumnType("int"); + + b.Property("NbModules") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("longtext"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("EdgeDevices"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ExternalIdentifier") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceModelId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ModuleName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeviceId") + .HasColumnType("varchar(255)"); + + b.Property("DeviceModelId") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceId") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceModelId") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("DeviceModelId"); + + b.HasIndex("EdgeDeviceId"); + + b.HasIndex("EdgeDeviceModelId"); + + b.ToTable("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Layer", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Father") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Planning") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("hasSub") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Layers"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("EnqueuedTime") + .HasColumnType("datetime(6)"); + + b.Property("LorawanDeviceId") + .HasColumnType("varchar(255)"); + + b.Property("Telemetry") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LorawanDeviceId"); + + b.ToTable("LoRaDeviceTelemetry"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DayOff") + .HasColumnType("int"); + + b.Property("End") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Frequency") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Start") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Plannings"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("End") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlanningId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Start") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("PlanningId"); + + b.ToTable("Schedules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("longtext"); + + b.Property("Xml") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasBaseType("IoTHub.Portal.Domain.Entities.Device"); + + b.Property("ABPRelaxMode") + .HasColumnType("tinyint(1)"); + + b.Property("AlreadyLoggedInOnce") + .HasColumnType("tinyint(1)"); + + b.Property("AppEUI") + .HasColumnType("longtext"); + + b.Property("AppKey") + .HasColumnType("longtext"); + + b.Property("AppSKey") + .HasColumnType("longtext"); + + b.Property("ClassType") + .HasColumnType("int"); + + b.Property("DataRate") + .HasColumnType("longtext"); + + b.Property("Deduplication") + .HasColumnType("int"); + + b.Property("DevAddr") + .HasColumnType("longtext"); + + b.Property("Downlink") + .HasColumnType("tinyint(1)"); + + b.Property("FCntDownStart") + .HasColumnType("int"); + + b.Property("FCntResetCounter") + .HasColumnType("int"); + + b.Property("FCntUpStart") + .HasColumnType("int"); + + b.Property("GatewayID") + .HasColumnType("longtext"); + + b.Property("KeepAliveTimeout") + .HasColumnType("int"); + + b.Property("NbRep") + .HasColumnType("longtext"); + + b.Property("NwkSKey") + .HasColumnType("longtext"); + + b.Property("PreferredWindow") + .HasColumnType("int"); + + b.Property("RX1DROffset") + .HasColumnType("int"); + + b.Property("RX2DataRate") + .HasColumnType("int"); + + b.Property("RXDelay") + .HasColumnType("int"); + + b.Property("ReportedRX1DROffset") + .HasColumnType("longtext"); + + b.Property("ReportedRX2DataRate") + .HasColumnType("longtext"); + + b.Property("ReportedRXDelay") + .HasColumnType("longtext"); + + b.Property("SensorDecoder") + .HasColumnType("longtext"); + + b.Property("Supports32BitFCnt") + .HasColumnType("tinyint(1)"); + + b.Property("TxPower") + .HasColumnType("longtext"); + + b.Property("UseOTAA") + .HasColumnType("tinyint(1)"); + + b.ToTable("LorawanDevices", (string)null); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Tags") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Tags") + .HasForeignKey("EdgeDeviceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Labels") + .HasForeignKey("DeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", null) + .WithMany("Labels") + .HasForeignKey("DeviceModelId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceModelId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.LorawanDevice", null) + .WithMany("Telemetry") + .HasForeignKey("LorawanDeviceId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Planning", null) + .WithMany("Schedules") + .HasForeignKey("PlanningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithOne() + .HasForeignKey("IoTHub.Portal.Domain.Entities.LorawanDevice", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.Navigation("Telemetry"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.cs b/src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.cs new file mode 100644 index 000000000..419c0eb85 --- /dev/null +++ b/src/IoTHub.Portal.MySql/Migrations/20241128154918_Delete cascade plannings & schedules.cs @@ -0,0 +1,60 @@ +// 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 IoTHub.Portal.MySql.Migrations +{ + /// + public partial class Deletecascadeplanningsschedules : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.AlterColumn( + name: "PlanningId", + table: "Schedules", + type: "varchar(255)", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + _ = migrationBuilder.CreateIndex( + name: "IX_Schedules_PlanningId", + table: "Schedules", + column: "PlanningId"); + + _ = migrationBuilder.AddForeignKey( + name: "FK_Schedules_Plannings_PlanningId", + table: "Schedules", + column: "PlanningId", + principalTable: "Plannings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropForeignKey( + name: "FK_Schedules_Plannings_PlanningId", + table: "Schedules"); + + _ = migrationBuilder.DropIndex( + name: "IX_Schedules_PlanningId", + table: "Schedules"); + + _ = migrationBuilder.AlterColumn( + name: "PlanningId", + table: "Schedules", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "varchar(255)") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.Designer.cs b/src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.Designer.cs new file mode 100644 index 000000000..f86fe68a5 --- /dev/null +++ b/src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.Designer.cs @@ -0,0 +1,715 @@ +// +using System; +using IoTHub.Portal.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IoTHub.Portal.MySql.Migrations +{ + [DbContext(typeof(PortalDbContext))] + [Migration("20241128160016_Add LastActivityTime")] + partial class AddLastActivityTime + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Concentrator", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ClientThumbprint") + .HasColumnType("longtext"); + + b.Property("DeviceType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsConnected") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LoraRegion") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Concentrators"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsConnected") + .HasColumnType("tinyint(1)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastActivityTime") + .HasColumnType("datetime(6)"); + + b.Property("LayerId") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StatusUpdatedTime") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("Devices", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ABPRelaxMode") + .HasColumnType("tinyint(1)"); + + b.Property("AppEUI") + .HasColumnType("longtext"); + + b.Property("ClassType") + .HasColumnType("int"); + + b.Property("Deduplication") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Downlink") + .HasColumnType("tinyint(1)"); + + b.Property("IsBuiltin") + .HasColumnType("tinyint(1)"); + + b.Property("KeepAliveTimeout") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PreferredWindow") + .HasColumnType("int"); + + b.Property("RXDelay") + .HasColumnType("int"); + + b.Property("SensorDecoder") + .HasColumnType("longtext"); + + b.Property("SupportLoRaFeatures") + .HasColumnType("tinyint(1)"); + + b.Property("UseOTAA") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("DeviceModels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Confirmed") + .HasColumnType("tinyint(1)"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Frame") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsBuiltin") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Port") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModelProperty", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsWritable") + .HasColumnType("tinyint(1)"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PropertyType") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelProperties"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTag", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Required") + .HasColumnType("tinyint(1)"); + + b.Property("Searchable") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("DeviceTags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("DeviceId") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceId") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("EdgeDeviceId"); + + b.ToTable("DeviceTagValues"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConnectionState") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NbDevices") + .HasColumnType("int"); + + b.Property("NbModules") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("longtext"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("EdgeDevices"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ExternalIdentifier") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceModelId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ModuleName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeviceId") + .HasColumnType("varchar(255)"); + + b.Property("DeviceModelId") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceId") + .HasColumnType("varchar(255)"); + + b.Property("EdgeDeviceModelId") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("DeviceModelId"); + + b.HasIndex("EdgeDeviceId"); + + b.HasIndex("EdgeDeviceModelId"); + + b.ToTable("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Layer", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("Father") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Planning") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("hasSub") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Layers"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("EnqueuedTime") + .HasColumnType("datetime(6)"); + + b.Property("LorawanDeviceId") + .HasColumnType("varchar(255)"); + + b.Property("Telemetry") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LorawanDeviceId"); + + b.ToTable("LoRaDeviceTelemetry"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DayOff") + .HasColumnType("int"); + + b.Property("End") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Frequency") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Start") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Plannings"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("End") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlanningId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Start") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("PlanningId"); + + b.ToTable("Schedules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("longtext"); + + b.Property("Xml") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasBaseType("IoTHub.Portal.Domain.Entities.Device"); + + b.Property("ABPRelaxMode") + .HasColumnType("tinyint(1)"); + + b.Property("AlreadyLoggedInOnce") + .HasColumnType("tinyint(1)"); + + b.Property("AppEUI") + .HasColumnType("longtext"); + + b.Property("AppKey") + .HasColumnType("longtext"); + + b.Property("AppSKey") + .HasColumnType("longtext"); + + b.Property("ClassType") + .HasColumnType("int"); + + b.Property("DataRate") + .HasColumnType("longtext"); + + b.Property("Deduplication") + .HasColumnType("int"); + + b.Property("DevAddr") + .HasColumnType("longtext"); + + b.Property("Downlink") + .HasColumnType("tinyint(1)"); + + b.Property("FCntDownStart") + .HasColumnType("int"); + + b.Property("FCntResetCounter") + .HasColumnType("int"); + + b.Property("FCntUpStart") + .HasColumnType("int"); + + b.Property("GatewayID") + .HasColumnType("longtext"); + + b.Property("KeepAliveTimeout") + .HasColumnType("int"); + + b.Property("NbRep") + .HasColumnType("longtext"); + + b.Property("NwkSKey") + .HasColumnType("longtext"); + + b.Property("PreferredWindow") + .HasColumnType("int"); + + b.Property("RX1DROffset") + .HasColumnType("int"); + + b.Property("RX2DataRate") + .HasColumnType("int"); + + b.Property("RXDelay") + .HasColumnType("int"); + + b.Property("ReportedRX1DROffset") + .HasColumnType("longtext"); + + b.Property("ReportedRX2DataRate") + .HasColumnType("longtext"); + + b.Property("ReportedRXDelay") + .HasColumnType("longtext"); + + b.Property("SensorDecoder") + .HasColumnType("longtext"); + + b.Property("Supports32BitFCnt") + .HasColumnType("tinyint(1)"); + + b.Property("TxPower") + .HasColumnType("longtext"); + + b.Property("UseOTAA") + .HasColumnType("tinyint(1)"); + + b.ToTable("LorawanDevices", (string)null); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Tags") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Tags") + .HasForeignKey("EdgeDeviceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Labels") + .HasForeignKey("DeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", null) + .WithMany("Labels") + .HasForeignKey("DeviceModelId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceModelId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.LorawanDevice", null) + .WithMany("Telemetry") + .HasForeignKey("LorawanDeviceId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Planning", null) + .WithMany("Schedules") + .HasForeignKey("PlanningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithOne() + .HasForeignKey("IoTHub.Portal.Domain.Entities.LorawanDevice", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.Navigation("Telemetry"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.cs b/src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.cs new file mode 100644 index 000000000..556b44a0b --- /dev/null +++ b/src/IoTHub.Portal.MySql/Migrations/20241128160016_Add LastActivityTime.cs @@ -0,0 +1,30 @@ +// 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 IoTHub.Portal.MySql.Migrations +{ + /// + public partial class AddLastActivityTime : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.AddColumn( + name: "LastActivityTime", + table: "Devices", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropColumn( + name: "LastActivityTime", + table: "Devices"); + } + } +} diff --git a/src/IoTHub.Portal.MySql/Migrations/PortalDbContextModelSnapshot.cs b/src/IoTHub.Portal.MySql/Migrations/PortalDbContextModelSnapshot.cs index 941e36a9e..48332c511 100644 --- a/src/IoTHub.Portal.MySql/Migrations/PortalDbContextModelSnapshot.cs +++ b/src/IoTHub.Portal.MySql/Migrations/PortalDbContextModelSnapshot.cs @@ -1,4 +1,10 @@ -// +// +using System; +using IoTHub.Portal.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable @@ -11,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -65,6 +71,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsEnabled") .HasColumnType("tinyint(1)"); + b.Property("LastActivityTime") + .HasColumnType("datetime(6)"); + b.Property("LayerId") .HasColumnType("longtext"); @@ -462,7 +471,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PlanningId") .IsRequired() - .HasColumnType("longtext"); + .HasColumnType("varchar(255)"); b.Property("Start") .IsRequired() @@ -470,6 +479,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("PlanningId"); + b.ToTable("Schedules"); }); @@ -644,6 +655,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("LorawanDeviceId"); }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Planning", null) + .WithMany("Schedules") + .HasForeignKey("PlanningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => { b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) @@ -677,6 +697,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Labels"); }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Navigation("Schedules"); + }); + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => { b.Navigation("Telemetry"); diff --git a/src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.Designer.cs b/src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.Designer.cs new file mode 100644 index 000000000..f48a5938b --- /dev/null +++ b/src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.Designer.cs @@ -0,0 +1,715 @@ +// +using System; +using IoTHub.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 IoTHub.Portal.Postgres.Migrations +{ + [DbContext(typeof(PortalDbContext))] + [Migration("20241128155624_Add LastActivityTime")] + partial class AddLastActivityTime + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Concentrator", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClientThumbprint") + .HasColumnType("text"); + + b.Property("DeviceType") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConnected") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LoraRegion") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Concentrators"); + }); + + modelBuilder.Entity("IoTHub.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("LastActivityTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LayerId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusUpdatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("Devices", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ABPRelaxMode") + .HasColumnType("boolean"); + + b.Property("AppEUI") + .HasColumnType("text"); + + b.Property("ClassType") + .HasColumnType("integer"); + + 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("IoTHub.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("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Port") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("DeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.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("IoTHub.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("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("EdgeDeviceId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("EdgeDeviceId"); + + b.ToTable("DeviceTagValues"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConnectionState") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NbDevices") + .HasColumnType("integer"); + + b.Property("NbModules") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeviceModelId"); + + b.ToTable("EdgeDevices"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ExternalIdentifier") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModelCommand", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("EdgeDeviceModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ModuleName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("EdgeDeviceModelCommands"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeviceModelId") + .HasColumnType("text"); + + b.Property("EdgeDeviceId") + .HasColumnType("text"); + + b.Property("EdgeDeviceModelId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("DeviceModelId"); + + b.HasIndex("EdgeDeviceId"); + + b.HasIndex("EdgeDeviceModelId"); + + b.ToTable("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Layer", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Father") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Planning") + .IsRequired() + .HasColumnType("text"); + + b.Property("hasSub") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Layers"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("EnqueuedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LorawanDeviceId") + .HasColumnType("text"); + + b.Property("Telemetry") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LorawanDeviceId"); + + b.ToTable("LoRaDeviceTelemetry"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DayOff") + .HasColumnType("integer"); + + b.Property("End") + .IsRequired() + .HasColumnType("text"); + + b.Property("Frequency") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Start") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Plannings"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CommandId") + .IsRequired() + .HasColumnType("text"); + + b.Property("End") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanningId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Start") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PlanningId"); + + b.ToTable("Schedules"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasBaseType("IoTHub.Portal.Domain.Entities.Device"); + + 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("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("KeepAliveTimeout") + .HasColumnType("integer"); + + 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("Supports32BitFCnt") + .HasColumnType("boolean"); + + b.Property("TxPower") + .HasColumnType("text"); + + b.Property("UseOTAA") + .HasColumnType("boolean"); + + b.ToTable("LorawanDevices", (string)null); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceTagValue", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Tags") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Tags") + .HasForeignKey("EdgeDeviceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", "DeviceModel") + .WithMany() + .HasForeignKey("DeviceModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceModel"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Label", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithMany("Labels") + .HasForeignKey("DeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.DeviceModel", null) + .WithMany("Labels") + .HasForeignKey("DeviceModelId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDevice", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceId"); + + b.HasOne("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", null) + .WithMany("Labels") + .HasForeignKey("EdgeDeviceModelId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LoRaDeviceTelemetry", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.LorawanDevice", null) + .WithMany("Telemetry") + .HasForeignKey("LorawanDeviceId"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Schedule", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Planning", null) + .WithMany("Schedules") + .HasForeignKey("PlanningId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.HasOne("IoTHub.Portal.Domain.Entities.Device", null) + .WithOne() + .HasForeignKey("IoTHub.Portal.Domain.Entities.LorawanDevice", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Device", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.DeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDevice", b => + { + b.Navigation("Labels"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.EdgeDeviceModel", b => + { + b.Navigation("Labels"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.Planning", b => + { + b.Navigation("Schedules"); + }); + + modelBuilder.Entity("IoTHub.Portal.Domain.Entities.LorawanDevice", b => + { + b.Navigation("Telemetry"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.cs b/src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.cs new file mode 100644 index 000000000..bd2531f8f --- /dev/null +++ b/src/IoTHub.Portal.Postgres/Migrations/20241128155624_Add LastActivityTime.cs @@ -0,0 +1,30 @@ +// 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 IoTHub.Portal.Postgres.Migrations +{ + /// + public partial class AddLastActivityTime : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.AddColumn( + name: "LastActivityTime", + table: "Devices", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropColumn( + name: "LastActivityTime", + table: "Devices"); + } + } +} diff --git a/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs b/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs index 31f1eefa1..6110f2822 100644 --- a/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs +++ b/src/IoTHub.Portal.Postgres/Migrations/PortalDbContextModelSnapshot.cs @@ -71,6 +71,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsEnabled") .HasColumnType("boolean"); + b.Property("LastActivityTime") + .HasColumnType("timestamp with time zone"); + b.Property("LayerId") .HasColumnType("text"); diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/DeviceDetails.cs b/src/IoTHub.Portal.Shared/Models/v1.0/DeviceDetails.cs index ab1e5ecb7..9981a6439 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/DeviceDetails.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/DeviceDetails.cs @@ -53,6 +53,11 @@ public class DeviceDetails : IDeviceDetails /// public DateTime StatusUpdatedTime { get; set; } + /// + /// Gets or sets the last activity time. + /// + public DateTime LastActivityTime { get; set; } + /// /// List of custom device tags and their values. /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/DeviceListItem.cs b/src/IoTHub.Portal.Shared/Models/v1.0/DeviceListItem.cs index 25a66763f..69e9ea953 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/DeviceListItem.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/DeviceListItem.cs @@ -53,6 +53,11 @@ public class DeviceListItem /// public DateTime StatusUpdatedTime { get; set; } + /// + /// Gets or sets the last activity time. + /// + public DateTime LastActivityTime { get; set; } + /// /// The device labels. /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/IDeviceDetails.cs b/src/IoTHub.Portal.Shared/Models/v1.0/IDeviceDetails.cs index 82cd84232..6a9e1c3f3 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/IDeviceDetails.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/IDeviceDetails.cs @@ -45,6 +45,11 @@ public interface IDeviceDetails /// public DateTime StatusUpdatedTime { get; set; } + /// + /// Gets or sets the last activity time. + /// + public DateTime LastActivityTime { get; set; } + /// /// List of custom device tags and their values. /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs index 5252b16a6..06d7e6640 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/LoRaDeviceDetails.cs @@ -45,6 +45,11 @@ public class LoRaDeviceDetails : LoRaDeviceBase, IDeviceDetails /// public DateTime StatusUpdatedTime { get; set; } + /// + /// Gets or sets the last activity time. + /// + public DateTime LastActivityTime { get; set; } + /// /// List of custom device tags and their values. /// diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Components/Concentrators/ConcentratorSearchTests.cs b/src/IoTHub.Portal.Tests.Unit/Client/Components/Concentrators/ConcentratorSearchTests.cs index 1158cb187..7928339dd 100644 --- a/src/IoTHub.Portal.Tests.Unit/Client/Components/Concentrators/ConcentratorSearchTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Client/Components/Concentrators/ConcentratorSearchTests.cs @@ -31,7 +31,6 @@ public void SearchConcentrators_ClickOnSearch_SearchIsFired() cut.WaitForElement("#searchKeyword").Change(searchKeyword); cut.WaitForElement("#searchStatusAll").Click(); - cut.WaitForElement("#searchStateAll").Click(); // Act cut.WaitForElement("#searchButton").Click(); @@ -62,7 +61,6 @@ public void SearchConcentrators_ClickOnReset_SearchKeyworkIsSetToEmptyAndSearchI cut.WaitForElement("#searchKeyword").Input(searchKeyword); cut.WaitForElement("#searchStatusAll").Click(); - cut.WaitForElement("#searchStateAll").Click(); // Act cut.WaitForElement("#resetSearch").Click(); diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs b/src/IoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs index bd3b54786..0e7439a64 100644 --- a/src/IoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs @@ -86,7 +86,6 @@ public async Task WhenResetFilterButtonClickShouldClearFilters() cut.WaitForElement("#searchID").NodeValue = Guid.NewGuid().ToString(); cut.WaitForElement("#searchStatusEnabled").Click(); - cut.WaitForElement("#searchStateDisconnected").Click(); cut.WaitForElement("#resetSearch").Click(); await Task.Delay(100); @@ -94,9 +93,7 @@ public async Task WhenResetFilterButtonClickShouldClearFilters() // Assert cut.WaitForAssertion(() => Assert.IsNull(cut.Find("#searchID").NodeValue)); cut.WaitForAssertion(() => Assert.AreEqual("false", cut.Find("#searchStatusEnabled").Attributes["aria-checked"].Value)); - cut.WaitForAssertion(() => Assert.AreEqual("false", cut.Find("#searchStateDisconnected").Attributes["aria-checked"].Value)); cut.WaitForAssertion(() => Assert.AreEqual("true", cut.Find("#searchStatusAll").Attributes["aria-checked"].Value)); - cut.WaitForAssertion(() => Assert.AreEqual("true", cut.Find("#searchStateAll").Attributes["aria-checked"].Value)); cut.WaitForAssertion(() => MockRepository.VerifyAll()); } diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Mappers/DeviceTwinMapperTests.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Mappers/DeviceTwinMapperTests.cs index ef9f3e6ce..7e15e7d87 100644 --- a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Mappers/DeviceTwinMapperTests.cs +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Mappers/DeviceTwinMapperTests.cs @@ -65,6 +65,7 @@ public void CreateDeviceDetailsStateUnderTestExpectedBehavior() Assert.AreEqual(DeviceModelImageOptions.DefaultImage, result.Image); Assert.AreEqual(DateTime.MinValue, result.StatusUpdatedTime); + Assert.AreEqual(DateTime.MinValue, result.LastActivityTime); this.mockRepository.VerifyAll(); } @@ -106,6 +107,7 @@ public void CreateDeviceDetailsNullTagListExpectedBehavior() Assert.AreEqual(DeviceModelImageOptions.DefaultImage, result.Image); Assert.AreEqual(DateTime.MinValue, result.StatusUpdatedTime); + Assert.AreEqual(DateTime.MinValue, result.LastActivityTime); this.mockRepository.VerifyAll(); } @@ -138,6 +140,7 @@ public void CreateDeviceListItemStateUnderTestExpectedBehavior() Assert.IsFalse(result.IsEnabled); Assert.AreEqual(DateTime.MinValue, result.StatusUpdatedTime); + Assert.AreEqual(DateTime.MinValue, result.LastActivityTime); this.mockRepository.VerifyAll(); } @@ -180,6 +183,7 @@ public void CreateDeviceListItemNullTagListExpectedBehavior() Assert.AreEqual(DeviceModelImageOptions.DefaultImage, result.Image); Assert.AreEqual(DateTime.MinValue, result.StatusUpdatedTime); + Assert.AreEqual(DateTime.MinValue, result.LastActivityTime); this.mockRepository.VerifyAll(); } From 72d53b094757e78ac71e73cbadf5ee51f84b9581 Mon Sep 17 00:00:00 2001 From: E068097 Date: Tue, 3 Dec 2024 17:27:35 +0100 Subject: [PATCH 17/17] #3023 startupOrder not supported in Edge Device Model schema --- .../Helpers/ConfigHelper.cs | 14 +++++++++++++- .../EdgeModels/EdgeModule/ModuleDialog.razor | 13 ++++++++++++- .../EdgeModule/SystemModuleDialog.razor | 15 +++++++++++++-- .../Services/ConfigService.cs | 1 + src/IoTHub.Portal.Shared/GlobalUsings.cs | 1 + .../Models/DevicePropertyType.cs | 2 +- .../Models/v1.0/EdgeModelSystemModule.cs | 2 ++ .../Models/v1.0/IoTEdgeModule.cs | 2 ++ .../Models/v1.0/IoTEdgeModule/ConfigModule.cs | 19 +++++++++++++++---- .../EdgeAgentPropertiesDesired.cs | 9 +++++++++ .../IoTEdgeModule/EdgeHubPropertiesDesired.cs | 4 ++++ .../Models/v1.0/LoRaWAN/Channel.cs | 6 ++++++ .../Models/v1.0/LoRaWAN/ClassType.cs | 2 +- .../Models/v1.0/LoRaWAN/DeduplicationMode.cs | 2 +- .../Models/v1.0/LoRaWAN/RouterConfig.cs | 11 +++++++++++ .../EdgeModels/SystemModuleDialogTest.cs | 1 + .../Helpers/ConfigHelperTest.cs | 7 ++++++- 17 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/IoTHub.Portal.Application/Helpers/ConfigHelper.cs b/src/IoTHub.Portal.Application/Helpers/ConfigHelper.cs index 30a6763c0..b175e0cfb 100644 --- a/src/IoTHub.Portal.Application/Helpers/ConfigHelper.cs +++ b/src/IoTHub.Portal.Application/Helpers/ConfigHelper.cs @@ -142,6 +142,7 @@ public static IoTEdgeModule CreateGatewayModule(Configuration config, JProperty ModuleName = module.Name, Image = module.Value["settings"]?["image"]?.Value(), ContainerCreateOptions = module.Value["settings"]?["createOptions"]?.Value(), + StartupOrder = module.Value["settings"]?["startupOrder"]?.Value() ?? 0, Status = module.Value["status"]?.Value(), }; @@ -229,6 +230,11 @@ public static Dictionary> GenerateModulesCon edgeAgentPropertiesDesired.SystemModules.EdgeAgent.Settings.CreateOptions = edgeModel.SystemModules.Single(x => x.Name == "edgeAgent").ContainerCreateOptions; } + if (edgeModel.SystemModules.Single(x => x.Name == "edgeAgent").StartupOrder > 0) + { + edgeAgentPropertiesDesired.SystemModules.EdgeAgent.Settings.StartupOrder = edgeModel.SystemModules.Single(x => x.Name == "edgeAgent").StartupOrder; + } + if (!string.IsNullOrEmpty(edgeModel.SystemModules.Single(x => x.Name == "edgeHub").Image)) { edgeAgentPropertiesDesired.SystemModules.EdgeHub.Settings.Image = edgeModel.SystemModules.Single(x => x.Name == "edgeHub").Image; @@ -241,6 +247,11 @@ public static Dictionary> GenerateModulesCon edgeAgentPropertiesDesired.SystemModules.EdgeHub.Settings.CreateOptions = edgeModel.SystemModules.Single(x => x.Name == "edgeHub").ContainerCreateOptions; } + if (edgeModel.SystemModules.Single(x => x.Name == "edgeHub").StartupOrder > 0) + { + edgeAgentPropertiesDesired.SystemModules.EdgeHub.Settings.StartupOrder = edgeModel.SystemModules.Single(x => x.Name == "edgeHub").StartupOrder; + } + foreach (var item in edgeModel.SystemModules.Single(x => x.Name == "edgeAgent").EnvironmentVariables) { edgeAgentPropertiesDesired.SystemModules.EdgeAgent.EnvironmentVariables?.Add(item.Name, new EnvironmentVariable() { EnvValue = item.Value }); @@ -262,7 +273,8 @@ public static Dictionary> GenerateModulesCon Settings = new ModuleSettings() { Image = module.Image, - CreateOptions = module.ContainerCreateOptions + CreateOptions = module.ContainerCreateOptions, + StartupOrder = module.StartupOrder, }, RestartPolicy = "always", EnvironmentVariables = new Dictionary() diff --git a/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/ModuleDialog.razor b/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/ModuleDialog.razor index dbc48e572..8c3a8a5b7 100644 --- a/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/ModuleDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/ModuleDialog.razor @@ -4,7 +4,7 @@ - + + + + currentEnvironmentVariables = new(); private List currentModuleIdentityTwinSettings = new(); @@ -103,6 +112,7 @@ currentImage = Module.Image; currentContainerCreateOptions = Module.ContainerCreateOptions; currentNumVersion = Module.Version; + currentStartupOrder = Module.StartupOrder; currentEnvironmentVariables = new List(Module.EnvironmentVariables.ToArray()); currentModuleIdentityTwinSettings = new List(Module.ModuleIdentityTwinSettings.ToArray()); currentCommands = new List(Module.Commands.ToArray()); @@ -115,6 +125,7 @@ Module.ModuleName = currentModuleName; Module.Image = currentImage; Module.ContainerCreateOptions = currentContainerCreateOptions; + Module.StartupOrder = currentStartupOrder; if (Portal.CloudProvider.Equals(CloudProviders.Azure)) { Module.Version = " "; } else { Module.Version = currentNumVersion; } diff --git a/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/SystemModuleDialog.razor b/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/SystemModuleDialog.razor index e1469ed9d..d7bfd8a76 100644 --- a/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/SystemModuleDialog.razor +++ b/src/IoTHub.Portal.Client/Dialogs/EdgeModels/EdgeModule/SystemModuleDialog.razor @@ -3,7 +3,7 @@ - + + + + -@code { + @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; @@ -55,6 +63,7 @@ private string currentModuleName = default!; private string currentImage = default!; private string currentContainerCreateOptions = default!; + private int currentStartupOrder; private List currentEnvironmentVariables = new(); @@ -67,6 +76,7 @@ currentModuleName = Module.Name; currentImage = Module.Image; currentContainerCreateOptions = Module.ContainerCreateOptions; + currentStartupOrder = Module.StartupOrder; currentEnvironmentVariables = new List(Module.EnvironmentVariables.ToArray()); await Task.Delay(0); IsLoading = false; @@ -77,6 +87,7 @@ Module.Name = currentModuleName; Module.Image = currentImage; Module.ContainerCreateOptions = currentContainerCreateOptions; + Module.StartupOrder = currentStartupOrder; Module.EnvironmentVariables = new List(currentEnvironmentVariables.ToArray()); MudDialog.Close(DialogResult.Ok(true)); } diff --git a/src/IoTHub.Portal.Server/Services/ConfigService.cs b/src/IoTHub.Portal.Server/Services/ConfigService.cs index a49fb22a6..129fc8064 100644 --- a/src/IoTHub.Portal.Server/Services/ConfigService.cs +++ b/src/IoTHub.Portal.Server/Services/ConfigService.cs @@ -125,6 +125,7 @@ public async Task> GetModelSystemModule(string model Image = newModule.Image, EnvironmentVariables = newModule.EnvironmentVariables, ContainerCreateOptions = newModule.ContainerCreateOptions, + StartupOrder = newModule.StartupOrder }); } } diff --git a/src/IoTHub.Portal.Shared/GlobalUsings.cs b/src/IoTHub.Portal.Shared/GlobalUsings.cs index 7a634020e..f7901d94d 100644 --- a/src/IoTHub.Portal.Shared/GlobalUsings.cs +++ b/src/IoTHub.Portal.Shared/GlobalUsings.cs @@ -10,3 +10,4 @@ global using IoTHub.Portal.Shared.Constants; global using IoTHub.Portal.Shared.Models; global using IoTHub.Portal.Shared.Models.v10; +global using Newtonsoft.Json; diff --git a/src/IoTHub.Portal.Shared/Models/DevicePropertyType.cs b/src/IoTHub.Portal.Shared/Models/DevicePropertyType.cs index 5582347fa..7e71a21be 100644 --- a/src/IoTHub.Portal.Shared/Models/DevicePropertyType.cs +++ b/src/IoTHub.Portal.Shared/Models/DevicePropertyType.cs @@ -7,7 +7,7 @@ namespace IoTHub.Portal.Models /// /// Device property type. /// - [JsonConverter(typeof(JsonStringEnumConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] public enum DevicePropertyType { /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/EdgeModelSystemModule.cs b/src/IoTHub.Portal.Shared/Models/v1.0/EdgeModelSystemModule.cs index 16f2b27de..29f996a4f 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/EdgeModelSystemModule.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/EdgeModelSystemModule.cs @@ -11,6 +11,8 @@ public class EdgeModelSystemModule public string ContainerCreateOptions { get; set; } = default!; + public int StartupOrder { get; set; } + public List EnvironmentVariables { get; set; } public EdgeModelSystemModule(string name) diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs index 5f810e620..eb517e42c 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule.cs @@ -27,6 +27,8 @@ public class IoTEdgeModule public string ContainerCreateOptions { get; set; } = default!; + public int StartupOrder { get; set; } + /// /// The module status. /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/ConfigModule.cs b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/ConfigModule.cs index 9348d02d6..b8e375696 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/ConfigModule.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/ConfigModule.cs @@ -8,19 +8,23 @@ namespace IoTHub.Portal.Shared.Models.v10.IoTEdgeModule public class ConfigModule { [JsonPropertyName("settings")] + [JsonProperty(PropertyName = "settings")] public ModuleSettings Settings { get; set; } [JsonPropertyName("type")] + [JsonProperty(PropertyName = "type")] public string Type { get; set; } = default!; [JsonPropertyName("env")] - //, NullValueHandling = NullValueHandling.Ignore)] + [JsonProperty(PropertyName = "env", NullValueHandling = NullValueHandling.Ignore)] public IDictionary? EnvironmentVariables { get; set; } - [JsonPropertyName("status")]/*, NullValueHandling = NullValueHandling.Ignore)]*/ + [JsonPropertyName("status")] + [JsonProperty(PropertyName = "status", NullValueHandling = NullValueHandling.Ignore)] public string? Status { get; set; } - [JsonPropertyName("restartPolicy")]/*, NullValueHandling = NullValueHandling.Ignore)]*/ + [JsonPropertyName("restartPolicy")] + [JsonProperty(PropertyName = "restartPolicy", NullValueHandling = NullValueHandling.Ignore)] public string? RestartPolicy { get; set; } public ConfigModule() @@ -32,15 +36,22 @@ public ConfigModule() public class ModuleSettings { [JsonPropertyName("image")] + [JsonProperty(PropertyName = "image")] public string Image { get; set; } = default!; - [JsonPropertyName("createOptions")]/*, NullValueHandling = NullValueHandling.Ignore)]*/ + [JsonPropertyName("createOptions")] + [JsonProperty(PropertyName = "createOptions", NullValueHandling = NullValueHandling.Ignore)] public string CreateOptions { get; set; } = default!; + + [JsonPropertyName("startupOrder")] + [JsonProperty(PropertyName = "startupOrder", NullValueHandling = NullValueHandling.Ignore)] + public int StartupOrder { get; set; } = 0; } public class EnvironmentVariable { [JsonPropertyName("value")] + [JsonProperty(PropertyName = "value")] public string EnvValue { get; set; } = default!; } } diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeAgentPropertiesDesired.cs b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeAgentPropertiesDesired.cs index ac75e39a9..e7fb74792 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeAgentPropertiesDesired.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeAgentPropertiesDesired.cs @@ -6,15 +6,19 @@ namespace IoTHub.Portal.Shared.Models.v10.IoTEdgeModule public class EdgeAgentPropertiesDesired { [JsonPropertyName("modules")] + [JsonProperty(PropertyName = "modules")] public IDictionary Modules { get; set; } [JsonPropertyName("runtime")] + [JsonProperty(PropertyName = "runtime")] public Runtime Runtime { get; set; } [JsonPropertyName("schemaVersion")] + [JsonProperty(PropertyName = "schemaVersion")] public string SchemaVersion { get; set; } [JsonPropertyName("systemModules")] + [JsonProperty(PropertyName = "systemModules")] public SystemModules SystemModules { get; set; } public EdgeAgentPropertiesDesired() @@ -29,9 +33,11 @@ public EdgeAgentPropertiesDesired() public class Runtime { [JsonPropertyName("settings")] + [JsonProperty(PropertyName = "settings")] public RuntimeSettings Settings { get; set; } [JsonPropertyName("type")] + [JsonProperty(PropertyName = "type")] public string Type { get; set; } public Runtime() @@ -44,6 +50,7 @@ public Runtime() public class RuntimeSettings { [JsonPropertyName("minDockerVersion")] + [JsonProperty(PropertyName = "minDockerVersion")] public string MinDockerVersion { get; set; } public RuntimeSettings() @@ -55,9 +62,11 @@ public RuntimeSettings() public class SystemModules { [JsonPropertyName("edgeAgent")] + [JsonProperty(PropertyName = "edgeAgent")] public ConfigModule EdgeAgent { get; set; } [JsonPropertyName("edgeHub")] + [JsonProperty(PropertyName = "edgeHub")] public ConfigModule EdgeHub { get; set; } public SystemModules() diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeHubPropertiesDesired.cs b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeHubPropertiesDesired.cs index 59dfe6c15..9b7b68c01 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeHubPropertiesDesired.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/IoTEdgeModule/EdgeHubPropertiesDesired.cs @@ -6,12 +6,15 @@ namespace IoTHub.Portal.Shared.Models.v10.IoTEdgeModule public class EdgeHubPropertiesDesired { [JsonPropertyName("routes")] + [JsonProperty(PropertyName = "routes")] public IDictionary Routes { get; set; } [JsonPropertyName("schemaVersion")] + [JsonProperty(PropertyName = "schemaVersion")] public string SchemaVersion { get; set; } [JsonPropertyName("storeAndForwardConfiguration")] + [JsonProperty(PropertyName = "storeAndForwardConfiguration")] public StoreAndForwardConfiguration StoreAndForwardConfiguration { get; set; } public EdgeHubPropertiesDesired() @@ -25,6 +28,7 @@ public EdgeHubPropertiesDesired() public class StoreAndForwardConfiguration { [JsonPropertyName("timeToLiveSecs")] + [JsonProperty(PropertyName = "timeToLiveSecs")] public int TimeToLiveSecs { get; set; } public StoreAndForwardConfiguration() diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/Channel.cs b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/Channel.cs index 864d3c9b7..70c5ec0af 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/Channel.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/Channel.cs @@ -12,36 +12,42 @@ public class Channel /// A value indicating whether the channel is enabled. /// [JsonPropertyName("enable")] + [JsonProperty("enable")] public bool? Enable { get; set; } /// /// The frequency. /// [JsonPropertyName("freq")] + [JsonProperty("freq")] public int Freq { get; set; } /// /// The radio. /// [JsonPropertyName("radio")] + [JsonProperty("radio")] public int Radio { get; set; } /// /// The interface. /// [JsonPropertyName("if")] + [JsonProperty("if")] public int If { get; set; } /// /// The bandwidth. /// [JsonPropertyName("bandwidth")] + [JsonProperty("bandwidth")] public int Bandwidth { get; set; } /// /// The spread factor. /// [JsonPropertyName("spread_factor")] + [JsonProperty("spread_factor")] public int SpreadFactor { get; set; } } } diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/ClassType.cs b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/ClassType.cs index ff50bca16..c4dac0217 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/ClassType.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/ClassType.cs @@ -6,7 +6,7 @@ namespace IoTHub.Portal.Models.v10.LoRaWAN /// /// LoRaWAN Device Class. /// - [JsonConverter(typeof(JsonStringEnumConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] public enum ClassType { /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/DeduplicationMode.cs b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/DeduplicationMode.cs index 5a64ef252..3ee107c05 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/DeduplicationMode.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/DeduplicationMode.cs @@ -6,7 +6,7 @@ namespace IoTHub.Portal.Models.v10.LoRaWAN /// /// LoRaWAN Deduplication strategy. /// - [JsonConverter(typeof(JsonStringEnumConverter))] + [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] public enum DeduplicationMode { /// diff --git a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/RouterConfig.cs b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/RouterConfig.cs index c4dcd0dbc..7acc24e1f 100644 --- a/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/RouterConfig.cs +++ b/src/IoTHub.Portal.Shared/Models/v1.0/LoRaWAN/RouterConfig.cs @@ -12,59 +12,70 @@ public class RouterConfig /// The network identifier. /// [JsonPropertyName("NetID")] + [JsonProperty("NetID")] public List NetID { get; set; } = new(); /// /// The join eui. /// [JsonPropertyName("JoinEui")] + [JsonProperty("JoinEui")] public List> JoinEui { get; set; } = new(); /// /// The region. /// [JsonPropertyName("region")] + [JsonProperty("region")] public string Region { get; set; } = default!; /// /// The hardware specifications. /// [JsonPropertyName("hwspec")] + [JsonProperty("hwspec")] public string Hwspec { get; set; } = default!; /// /// The frequency range. /// [JsonPropertyName("freq_range")] + [JsonProperty("freq_range")] public List FreqRange { get; set; } = new(); /// /// The DRs. /// [JsonPropertyName("DRs")] + [JsonProperty("DRs")] public List> DRs { get; set; } = new(); /// /// The SX1301 conf. /// [JsonPropertyName("sx1301_conf")] + [JsonProperty("sx1301_conf")] public List> Sx1301Conf { get; set; } = new(); /// /// true if nocca; otherwise, false. /// [JsonPropertyName("nocca")] + [JsonProperty("nocca")] public bool Nocca { get; set; } /// /// true if nodc; otherwise, false. /// [JsonPropertyName("nodc")] + [JsonProperty("nodc")] public bool Nodc { get; set; } + /// /// true if nodwell; otherwise, false. /// [JsonPropertyName("nodwell")] + [JsonProperty("nodwell")] public bool Nodwell { get; set; } } } diff --git a/src/IoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/SystemModuleDialogTest.cs b/src/IoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/SystemModuleDialogTest.cs index 4c2a883e7..fe0a10b20 100644 --- a/src/IoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/SystemModuleDialogTest.cs +++ b/src/IoTHub.Portal.Tests.Unit/Client/Pages/EdgeModels/SystemModuleDialogTest.cs @@ -27,6 +27,7 @@ public async Task SystemModuleDialogMustCloseOnCLickOnCloseButton() } }, ContainerCreateOptions = Fixture.Create(), + StartupOrder = Fixture.Create(), }; var cut = RenderComponent(); diff --git a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Helpers/ConfigHelperTest.cs b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Helpers/ConfigHelperTest.cs index e3248e2e6..100a490e6 100644 --- a/src/IoTHub.Portal.Tests.Unit/Infrastructure/Helpers/ConfigHelperTest.cs +++ b/src/IoTHub.Portal.Tests.Unit/Infrastructure/Helpers/ConfigHelperTest.cs @@ -86,7 +86,8 @@ public void CreateGatewayModuleShouldReturnValue() { "settings", new Dictionary() { { "image", "image_test" }, - { "createOptions", expectedContainerCreateOptions } + { "createOptions", expectedContainerCreateOptions }, + { "startupOrder", 100 }, } }, { "env", new Dictionary() @@ -111,6 +112,7 @@ public void CreateGatewayModuleShouldReturnValue() Assert.AreEqual("running", result.Status); Assert.AreEqual("image_test", result.Image); Assert.AreEqual(expectedContainerCreateOptions, result.ContainerCreateOptions); + Assert.AreEqual(100, result.StartupOrder); Assert.AreEqual(1, result.EnvironmentVariables.Count); } @@ -161,6 +163,7 @@ public void GenerateModulesContentShouldReturnValue() Status = "running", Image = "image", ContainerCreateOptions = expectedContainerCreateOptions, + StartupOrder = 100, EnvironmentVariables = new List() { new() { Name = "envTest01", Value = "test" } @@ -174,6 +177,7 @@ public void GenerateModulesContentShouldReturnValue() { Image = "image", ContainerCreateOptions = Guid.NewGuid().ToString(), + StartupOrder = 100, EnvironmentVariables = new List() { new IoTEdgeModuleEnvironmentVariable(){ Name ="test", Value = "test" } @@ -201,6 +205,7 @@ public void GenerateModulesContentShouldReturnValue() var edgeHubPropertiesDesired = (EdgeAgentPropertiesDesired)result["$edgeAgent"]["properties.desired"]; _ = edgeHubPropertiesDesired.Modules[expectedModuleName].Settings.CreateOptions.Should() .Be(expectedContainerCreateOptions); + _ = edgeHubPropertiesDesired.Modules[expectedModuleName].Settings.StartupOrder.Should().Be(100); } [Test]