From d223fe4b806ea2ed9951b4caca75e54e30dfa4b0 Mon Sep 17 00:00:00 2001 From: E068097 Date: Thu, 31 Oct 2024 11:28:12 +0100 Subject: [PATCH] 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(); + } + } +}