diff --git a/.github/workflows/develop_edge-dev-01-dittobox.yml b/.github/workflows/develop_edge-dev-01-dittobox.yml new file mode 100644 index 0000000..b9a673b --- /dev/null +++ b/.github/workflows/develop_edge-dev-01-dittobox.yml @@ -0,0 +1,65 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy ASP.Net Core app to Azure Web App - edge-dev-01-dittobox + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + - name: Build with dotnet + run: dotnet build --configuration Release + + - name: dotnet publish + run: dotnet publish -c Release -o ${{env.DOTNET_ROOT}}/myapp + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: .net-app + path: ${{env.DOTNET_ROOT}}/myapp + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: .net-app + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_233090D111F94CF38A1844536FC9A017 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_C1F177FC6AF84878AEF8459FD4E0E6C7 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_85141903D76C46FC8AE3CB521739A05E }} + + - name: Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'edge-dev-01-dittobox' + slot-name: 'Production' + package: . + \ No newline at end of file diff --git a/ContainerManagement/Application/ContainerSelfRegisterCommand.cs b/ContainerManagement/Application/ContainerSelfRegisterCommand.cs new file mode 100644 index 0000000..3ba8821 --- /dev/null +++ b/ContainerManagement/Application/ContainerSelfRegisterCommand.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace DittoBox.EdgeServer.ContainerManagement.Application +{ + public record ContainerSelfRegisterCommand + { + [Required] + public string Uiid { get; set; } + } +} diff --git a/ContainerManagement/Application/ContainerStatusReportCommand.cs b/ContainerManagement/Application/ContainerStatusReportCommand.cs index 2c58624..2e72d84 100644 --- a/ContainerManagement/Application/ContainerStatusReportCommand.cs +++ b/ContainerManagement/Application/ContainerStatusReportCommand.cs @@ -1,24 +1,33 @@ using System.ComponentModel.DataAnnotations; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.ValueObjects; namespace DittoBox.EdgeServer.ContainerManagement.Application { public record ContainerStatusReportCommand { + [Required] + public int ContainerId { get; set; } [Required] - public int ContainerId; + public string? DeviceId { get; set; } [Required] public double? Temperature { get; set; } [Required] public double? Humidity { get; set; } [Required] - public double? Oxygen { get; set; } + public double? GasOxygen { get; set; } [Required] - public double? Dioxide { get; set; } + public double? GasCO2 { get; set; } [Required] - public double? Ethylene { get; set; } + public double? GasEthylene { get; set; } [Required] - public double? Ammonia { get; set; } + public double? GasAmmonia { get; set; } [Required] - public double? SulfurDioxide { get; set; } + public double? GasSO2 { get; set; } + [Required] + public HealthMonitor GasHealthMonitor { get; set; } = new HealthMonitor(); + [Required] + public HealthMonitor TemperatureHealthMonitor { get; set; } = new HealthMonitor(); + [Required] + public HealthMonitor HumidityHealthMonitor { get; set; } = new HealthMonitor(); } } diff --git a/ContainerManagement/Application/GetContainersQuery.cs b/ContainerManagement/Application/GetContainersQuery.cs new file mode 100644 index 0000000..5e2caa2 --- /dev/null +++ b/ContainerManagement/Application/GetContainersQuery.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace DittoBox.EdgeServer.ContainerManagement.Application +{ + public record GetContainersQuery + { + } +} diff --git a/ContainerManagement/Application/Handlers/Interfaces/IContainerSelfRegisterCommandHandler.cs b/ContainerManagement/Application/Handlers/Interfaces/IContainerSelfRegisterCommandHandler.cs new file mode 100644 index 0000000..895c12b --- /dev/null +++ b/ContainerManagement/Application/Handlers/Interfaces/IContainerSelfRegisterCommandHandler.cs @@ -0,0 +1,9 @@ +using DittoBox.EdgeServer.ContainerManagement.Application.Resources; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces +{ + public interface IContainerSelfRegisterCommandHandler + { + Task Handle(ContainerSelfRegisterCommand command); + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/Handlers/Interfaces/IContainerStatusReportCommandHandler.cs b/ContainerManagement/Application/Handlers/Interfaces/IContainerStatusReportCommandHandler.cs new file mode 100644 index 0000000..c601845 --- /dev/null +++ b/ContainerManagement/Application/Handlers/Interfaces/IContainerStatusReportCommandHandler.cs @@ -0,0 +1,7 @@ +namespace DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces +{ + public interface IContainerStatusReportCommandHandler + { + Task Handle(ContainerStatusReportCommand command); + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/Handlers/Interfaces/IGetContainersQueryHandler.cs b/ContainerManagement/Application/Handlers/Interfaces/IGetContainersQueryHandler.cs new file mode 100644 index 0000000..6eed271 --- /dev/null +++ b/ContainerManagement/Application/Handlers/Interfaces/IGetContainersQueryHandler.cs @@ -0,0 +1,9 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces +{ + public interface IGetContainersQueryHandler + { + Task> Handle(); + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/Handlers/Internal/ContainerSelfRegisterCommandHandler.cs b/ContainerManagement/Application/Handlers/Internal/ContainerSelfRegisterCommandHandler.cs new file mode 100644 index 0000000..375bb1c --- /dev/null +++ b/ContainerManagement/Application/Handlers/Internal/ContainerSelfRegisterCommandHandler.cs @@ -0,0 +1,20 @@ +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces; +using DittoBox.EdgeServer.ContainerManagement.Application.Resources; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Internal +{ + public class ContainerSelfRegisterCommandHandler( + ICloudService cloudService, + IUnitOfWork unitOfWork + ) : IContainerSelfRegisterCommandHandler + { + public async Task Handle(ContainerSelfRegisterCommand command) + { + var result = await cloudService.RegisterContainer(command.Uiid); + await unitOfWork.CommitAsync(); + return result; + } + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/Handlers/Internal/ContainerStatusReportCommandHandler.cs b/ContainerManagement/Application/Handlers/Internal/ContainerStatusReportCommandHandler.cs new file mode 100644 index 0000000..f23f93e --- /dev/null +++ b/ContainerManagement/Application/Handlers/Internal/ContainerStatusReportCommandHandler.cs @@ -0,0 +1,106 @@ +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.ValueObjects; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Internal +{ + public class ContainerStatusReportCommandHandler( + IContainerService containerService, + ICloudService cloudService, + IUnitOfWork unitOfWork + ) : IContainerStatusReportCommandHandler + { + private readonly int _maxMillisecondsBetweenReports = Convert.ToInt32(Environment.GetEnvironmentVariable("MAX_MILLISECONDS_BETWEEN_REPORTS") ?? "60000"); + + public async Task Handle(ContainerStatusReportCommand command) + { + Container? container = await containerService.GetContainerById(command.ContainerId); + + // If not found by Id, try to find by DeviceId + if (container == null && !string.IsNullOrEmpty(command.DeviceId)) + { + container = await containerService.GetContainerByUIID(command.DeviceId); + } + + // If still not found, throw an exception + if (container == null) + { + throw new Exception("Container not found. Couldn't create a new container."); + } + + // Generate the status report + var statusRecord = new ContainerStatusRecord + { + ContainerId = container.Id, + Temperature = command.Temperature, + Humidity = command.Humidity, + GasOxygen = command.GasOxygen, + GasCO2 = command.GasCO2, + GasEthylene = command.GasEthylene, + GasAmmonia = command.GasAmmonia, + GasSO2 = command.GasSO2, + SavedAt = DateTime.Now + }; + + // Generate the health reports + var healthRecords = new List + { + new() { + ContainerId = container.Id, + SensorType = SensorType.GAS_SENSOR, + FailuresSinceStartup = command.GasHealthMonitor.FailuresSinceStartup, + FailuresSinceLastCheck = command.GasHealthMonitor.FailuresSinceLastCheck, + RequestsSinceLastCheck = command.GasHealthMonitor.RequestsSinceLastCheck, + RequestsSinceStartup = command.GasHealthMonitor.RequestsSinceStartup, + FailingRate = command.GasHealthMonitor.FailingRate, + SavedAt = DateTime.Now + }, + new() { + ContainerId = container.Id, + SensorType = SensorType.TEMPERATURE_SENSOR, + FailuresSinceStartup = command.TemperatureHealthMonitor.FailuresSinceStartup, + FailuresSinceLastCheck = command.TemperatureHealthMonitor.FailuresSinceLastCheck, + RequestsSinceLastCheck = command.TemperatureHealthMonitor.RequestsSinceLastCheck, + RequestsSinceStartup = command.TemperatureHealthMonitor.RequestsSinceStartup, + FailingRate = command.TemperatureHealthMonitor.FailingRate, + SavedAt = DateTime.Now + }, + new() { + ContainerId = container.Id, + SensorType = SensorType.HUMIDITY_SENSOR, + FailuresSinceStartup = command.HumidityHealthMonitor.FailuresSinceStartup, + FailuresSinceLastCheck = command.HumidityHealthMonitor.FailuresSinceLastCheck, + RequestsSinceLastCheck = command.HumidityHealthMonitor.RequestsSinceLastCheck, + RequestsSinceStartup = command.HumidityHealthMonitor.RequestsSinceStartup, + FailingRate = command.HumidityHealthMonitor.FailingRate, + SavedAt = DateTime.Now + } + }; + + // Save the health reports + foreach (var healthRecord in healthRecords) + { + await containerService.SaveHealthReport(healthRecord); + } + + // Save the status report + await containerService.SaveStatusReport(statusRecord); + + // Commit the changes + await unitOfWork.CommitAsync(); + + // ---- + + // Send the report to the cloud if necessary + if (container.LastSentStatusReport == null || container.LastSentStatusReport < DateTime.Now.AddMilliseconds(-_maxMillisecondsBetweenReports)) + { + await containerService.SendReportToCloud(container.Id, _maxMillisecondsBetweenReports); + + } + + await unitOfWork.CommitAsync(); + } + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/Handlers/Internal/GetContainersQueryHandler.cs b/ContainerManagement/Application/Handlers/Internal/GetContainersQueryHandler.cs new file mode 100644 index 0000000..752193e --- /dev/null +++ b/ContainerManagement/Application/Handlers/Internal/GetContainersQueryHandler.cs @@ -0,0 +1,17 @@ +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Internal +{ + public class GetContainersQueryHandler( + IContainerService containerService + ) : IGetContainersQueryHandler + { + public async Task> Handle() + { + return await containerService.GetContainers(); + } + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/HealthMonitor.cs b/ContainerManagement/Application/HealthMonitor.cs new file mode 100644 index 0000000..1b73532 --- /dev/null +++ b/ContainerManagement/Application/HealthMonitor.cs @@ -0,0 +1,16 @@ + +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.ValueObjects; + +namespace DittoBox.EdgeServer.ContainerManagement.Application +{ + public class HealthMonitor + { + public SensorType? SensorType { get; set; } + public int FailuresSinceStartup { get; set; } + public int FailuresSinceLastCheck { get; set; } + public int RequestsSinceLastCheck { get; set; } + public int RequestsSinceStartup { get; set; } + public double FailingRate { get; set; } + public DateTime SavedAt { get; set; } + } +} \ No newline at end of file diff --git a/ContainerManagement/Application/Resources/ContainerRegistrationResource.cs b/ContainerManagement/Application/Resources/ContainerRegistrationResource.cs new file mode 100644 index 0000000..9236432 --- /dev/null +++ b/ContainerManagement/Application/Resources/ContainerRegistrationResource.cs @@ -0,0 +1,8 @@ +namespace DittoBox.EdgeServer.ContainerManagement.Application.Resources +{ + public record ContainerRegistrationResource + { + public int Id { get; init; } + public string Uiid { get; init; } + } +} diff --git a/ContainerManagement/Application/Resources/Outbound/ContainerMetricsResource.cs b/ContainerManagement/Application/Resources/Outbound/ContainerMetricsResource.cs new file mode 100644 index 0000000..ce3fbd7 --- /dev/null +++ b/ContainerManagement/Application/Resources/Outbound/ContainerMetricsResource.cs @@ -0,0 +1,29 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Resources.Outbound +{ + public record ContainerMetricsResource + { + public double? Temperature { get; init; } + public double? Humidity { get; init; } + public double? Oxygen { get; init; } + public double? Dioxide { get; init; } + public double? Ethylene { get; init; } + public double? Ammonia { get; init; } + public double? SulfurDioxide { get; init; } + + public static ContainerMetricsResource FromContainerStatusRecord(ContainerStatusRecord containerStatusReport) + { + return new ContainerMetricsResource() + { + Temperature = containerStatusReport.Temperature, + Humidity = containerStatusReport.Humidity, + Oxygen = containerStatusReport.GasOxygen, + Dioxide = containerStatusReport.GasCO2, + Ethylene = containerStatusReport.GasEthylene, + Ammonia = containerStatusReport.GasAmmonia, + SulfurDioxide = containerStatusReport.GasSO2 + }; + } + } +} diff --git a/ContainerManagement/Application/Services/BaseService.cs b/ContainerManagement/Application/Services/BaseService.cs index d93cccc..a101737 100644 --- a/ContainerManagement/Application/Services/BaseService.cs +++ b/ContainerManagement/Application/Services/BaseService.cs @@ -2,6 +2,7 @@ { public abstract class BaseService { - protected string BaseUrl { get; set; } = "https://app-dev-01-dittobox-a8bpd5bkh4dnh3g7.eastus-01.azurewebsites.net/"; - } + protected string BaseUrl { get; set; } = "https://app-prod-01-dittobox-argeesg8era0c7ex.eastus-01.azurewebsites.net/api/v1/"; + + } } diff --git a/ContainerManagement/Application/Services/CloudService.cs b/ContainerManagement/Application/Services/CloudService.cs index 89972a2..2a24e6e 100644 --- a/ContainerManagement/Application/Services/CloudService.cs +++ b/ContainerManagement/Application/Services/CloudService.cs @@ -1,8 +1,34 @@ -using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Application.Resources; namespace DittoBox.EdgeServer.ContainerManagement.Application.Services { - public class CloudService : BaseService, ICloudService - { - } + public class CloudService( + IContainerService containerService + ) : BaseService, ICloudService + { + public Task SendContainerStatusReport(ContainerStatusRecord record) + { + throw new NotImplementedException(); + } + + public async Task RegisterContainer(string uiid) + { + var resource = new { uiid = uiid }; + var client = new HttpClient(); + var response = await client.PostAsJsonAsync(Path.Combine(BaseUrl, "group/register-container"), resource); + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(); + await containerService.CreateContainer(result.Uiid, result.Id); + return result; + } + else + { + Console.WriteLine($"Failed to register container: {response.StatusCode}. {response.Content}. \n\n {resource} "); + throw new Exception("Failed to register container"); + } + } + } } diff --git a/ContainerManagement/Application/Services/ContainerService.cs b/ContainerManagement/Application/Services/ContainerService.cs index ccd2868..520d649 100644 --- a/ContainerManagement/Application/Services/ContainerService.cs +++ b/ContainerManagement/Application/Services/ContainerService.cs @@ -1,8 +1,96 @@ -using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Application.Resources.Outbound; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; namespace DittoBox.EdgeServer.ContainerManagement.Application.Services { - public class ContainerService : BaseService, IContainerService - { + public class ContainerService( + IContainerRepository containerRepository, + IContainerHealthRecordRepository containerHealthRecordRepository, + IContainerStatusRecordRepository containerStatusRecordRepository, + ILogger logger + ) : BaseService, IContainerService + { + public async Task CreateContainer(string uiid, int idInCloud) + { + var container = new Container() { UIID = uiid, IdInCloudService = idInCloud }; + await containerRepository.Add(container); + return container; + } + + public Task ForwardNewTemplateSettings() + { + throw new NotImplementedException(); + } + + public async Task GetContainerById(int containerId) + { + return await containerRepository.GetById(containerId); + } + + public Task GetContainerByUIID(string uiid) + { + return containerRepository.GetContainerByUIID(uiid); + } + + public async Task> GetContainers() + { + return await containerRepository.GetAll(); + } + + public Task IsReportToCloudRequired(int containerId) + { + throw new NotImplementedException(); + } + + public async Task SaveHealthReport(ContainerHealthRecord healthReport) + { + await containerHealthRecordRepository.Add(healthReport); + } + + public async Task SaveStatusReport(ContainerStatusRecord statusReport) + { + await containerStatusRecordRepository.Add(statusReport); + } + + public async Task SendReportToCloud(int containerId, int timeframe) + { + // Send a report with a POST request to the cloud + var container = await containerRepository.GetById(containerId); + // Get the health report from the last 1 minute + var from = DateTime.Now.AddMinutes(-timeframe); + var statusReports = await containerStatusRecordRepository.GetLatestReportsByTime(containerId, from); + + // Send the report to the cloud + var client = new HttpClient(); + // Make a ContainerMetricsResource by the average of all the health reports + var statusResource = new ContainerMetricsResource() + { + Temperature = statusReports.Average(r => r.Temperature), + Humidity = statusReports.Average(r => r.Humidity), + Oxygen = statusReports.Average(r => r.GasOxygen), + Dioxide = statusReports.Average(r => r.GasCO2), + Ethylene = statusReports.Average(r => r.GasEthylene), + Ammonia = statusReports.Average(r => r.GasAmmonia), + SulfurDioxide = statusReports.Average(r => r.GasSO2) + }; + + var response = await client.PutAsJsonAsync(Path.Combine(BaseUrl, $"container/{container!.IdInCloudService}/metrics"),statusResource); + if (response.IsSuccessStatusCode) + { + + container.LastSentStatusReport = DateTime.Now; + await containerRepository.Update(container); + logger.LogInformation($"Status report sent to cloud for container {container.Id} at {container.LastSentStatusReport}"); + return; + } + else + { + logger.LogError($"Failed to send report to cloud. Status code: {response.StatusCode}.\n{response.Content}"); + throw new Exception($"Failed to send report to cloud."); + } + } } } diff --git a/ContainerManagement/Domain/Models/Entities/Container.cs b/ContainerManagement/Domain/Models/Entities/Container.cs index 8d32cf3..5d5146e 100644 --- a/ContainerManagement/Domain/Models/Entities/Container.cs +++ b/ContainerManagement/Domain/Models/Entities/Container.cs @@ -3,8 +3,9 @@ namespace DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; public class Container { public int Id { get; set; } + public int IdInCloudService { get; set; } // Id of the container in the cloud service. public string UIID { get; set; } // Unique internal identifier. Hardcoded value on each container microcontroller. public string? MACAddress { get; set; } // MAC address of the container microcontroller. - public DateTime LastReport { get; set; } // Last time the container microcontroller reported to the cloud. - public string? Name { get; set; } // Name of the container. + public DateTime? LastSentStatusReport { get; set; } // Last time a the container's status report was sent to the cloud. + public DateTime? LastSentHealthReport { get; set; } // Last time the container's health report was sent to the cloud. } \ No newline at end of file diff --git a/ContainerManagement/Domain/Models/Entities/ContainerHealthRecord.cs b/ContainerManagement/Domain/Models/Entities/ContainerHealthRecord.cs new file mode 100644 index 0000000..e3121b3 --- /dev/null +++ b/ContainerManagement/Domain/Models/Entities/ContainerHealthRecord.cs @@ -0,0 +1,18 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.ValueObjects; + +namespace DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; + +public class ContainerHealthRecord +{ + public int Id { get; set; } + public int ContainerId { get; set; } + public SensorType SensorType { get; set; } + public int FailuresSinceStartup { get; set; } + public int FailuresSinceLastCheck { get; set; } + public int RequestsSinceLastCheck { get; set; } + public int RequestsSinceStartup { get; set; } + public double FailingRate { get; set; } + public DateTime SavedAt { get; set; } + + public ContainerHealthRecord() { } +} \ No newline at end of file diff --git a/ContainerManagement/Domain/Models/Entities/ContainerStatusRecord.cs b/ContainerManagement/Domain/Models/Entities/ContainerStatusRecord.cs new file mode 100644 index 0000000..231da96 --- /dev/null +++ b/ContainerManagement/Domain/Models/Entities/ContainerStatusRecord.cs @@ -0,0 +1,17 @@ +namespace DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; + +public class ContainerStatusRecord +{ + public int Id { get; set; } + public int ContainerId { get; set; } + public double? Temperature { get; set; } + public double? Humidity { get; set; } + public double? GasOxygen { get; set; } + public double? GasCO2 { get; set; } + public double? GasEthylene { get; set; } + public double? GasAmmonia { get; set; } + public double? GasSO2 { get; set; } + public DateTime SavedAt { get; set; } + + public ContainerStatusRecord() { } +} diff --git a/ContainerManagement/Domain/Models/ValueObjects/SensorType.cs b/ContainerManagement/Domain/Models/ValueObjects/SensorType.cs new file mode 100644 index 0000000..6c246ed --- /dev/null +++ b/ContainerManagement/Domain/Models/ValueObjects/SensorType.cs @@ -0,0 +1,7 @@ +namespace DittoBox.EdgeServer.ContainerManagement.Domain.Models.ValueObjects; + +public enum SensorType { + TEMPERATURE_SENSOR, + HUMIDITY_SENSOR, + GAS_SENSOR +} \ No newline at end of file diff --git a/ContainerManagement/Domain/Services/ICloudService.cs b/ContainerManagement/Domain/Services/ICloudService.cs index 8f31115..56f2986 100644 --- a/ContainerManagement/Domain/Services/ICloudService.cs +++ b/ContainerManagement/Domain/Services/ICloudService.cs @@ -1,6 +1,12 @@ -namespace DittoBox.EdgeServer.ContainerManagement.Domain.Services +using DittoBox.EdgeServer.ContainerManagement.Application.Resources; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; + +namespace DittoBox.EdgeServer.ContainerManagement.Domain.Services { - public interface ICloudService - { - } + public interface ICloudService + { + public Task SendContainerStatusReport(ContainerStatusRecord record); + public Task RegisterContainer(string uiid); + + } } diff --git a/ContainerManagement/Domain/Services/IContainerService.cs b/ContainerManagement/Domain/Services/IContainerService.cs index eecb4b5..419ffd8 100644 --- a/ContainerManagement/Domain/Services/IContainerService.cs +++ b/ContainerManagement/Domain/Services/IContainerService.cs @@ -1,9 +1,18 @@ using DittoBox.EdgeServer.ContainerManagement.Application.Services; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; namespace DittoBox.EdgeServer.ContainerManagement.Domain.Services { public interface IContainerService { - + public Task SaveHealthReport(ContainerHealthRecord healthReport); + public Task SaveStatusReport(ContainerStatusRecord statusReport); + public Task IsReportToCloudRequired(int containerId); + public Task ForwardNewTemplateSettings(); + public Task GetContainerById(int containerId); + public Task> GetContainers(); + public Task GetContainerByUIID(string uiid); + public Task CreateContainer(string uiid, int idInCloud); + public Task SendReportToCloud(int containerId, int timeframe); } } diff --git a/ContainerManagement/Infrastructure/Configuration/ApplicationDbContext.cs b/ContainerManagement/Infrastructure/Configuration/ApplicationDbContext.cs index c53a415..c3c8943 100644 --- a/ContainerManagement/Infrastructure/Configuration/ApplicationDbContext.cs +++ b/ContainerManagement/Infrastructure/Configuration/ApplicationDbContext.cs @@ -2,15 +2,9 @@ namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; using Microsoft.EntityFrameworkCore; -public class ApplicationDbContext : DbContext +public class ApplicationDbContext(DbContextOptions options) : DbContext(options) { - public ApplicationDbContext(DbContextOptions options) : base(options) - { - } - public DbSet Containers { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - } + public DbSet ContainerStatusRecords { get; set; } + public DbSet ContainerHealthRecords { get; set; } } \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Configuration/IUnitOfWork.cs b/ContainerManagement/Infrastructure/Configuration/IUnitOfWork.cs new file mode 100644 index 0000000..fa1bb0b --- /dev/null +++ b/ContainerManagement/Infrastructure/Configuration/IUnitOfWork.cs @@ -0,0 +1,5 @@ +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; + +public interface IUnitOfWork { + Task CommitAsync(); +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Configuration/UnitOfWork.cs b/ContainerManagement/Infrastructure/Configuration/UnitOfWork.cs new file mode 100644 index 0000000..64e7928 --- /dev/null +++ b/ContainerManagement/Infrastructure/Configuration/UnitOfWork.cs @@ -0,0 +1,12 @@ + +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; + +public class UnitOfWork(ApplicationDbContext context) : IUnitOfWork +{ + private readonly ApplicationDbContext context = context; + + public async Task CommitAsync() + { + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/BaseRepository.cs b/ContainerManagement/Infrastructure/Repositories/BaseRepository.cs index ea4c33f..7bd0f9e 100644 --- a/ContainerManagement/Infrastructure/Repositories/BaseRepository.cs +++ b/ContainerManagement/Infrastructure/Repositories/BaseRepository.cs @@ -2,40 +2,42 @@ using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; using Microsoft.EntityFrameworkCore; -public abstract class BaseRepository : IBaseRepository where T : class + +public abstract class BaseRepository( + ApplicationDbContext context + ) : IBaseRepository where T : class { - protected readonly ApplicationDbContext _dbContext; - - public BaseRepository(ApplicationDbContext dbContext) - { - _dbContext = dbContext; - } - - public virtual async Task GetByIdAsync(Guid id) - { - return await _dbContext.Set().FindAsync(id); - } - - public virtual async Task> GetAllAsync() - { - return await _dbContext.Set().ToListAsync(); - } - - public virtual async Task AddAsync(T entity) - { - await _dbContext.Set().AddAsync(entity); - await _dbContext.SaveChangesAsync(); - } - - public virtual async Task UpdateAsync(T entity) - { - _dbContext.Set().Update(entity); - await _dbContext.SaveChangesAsync(); - } - - public virtual async Task DeleteAsync(T entity) - { - _dbContext.Set().Remove(entity); - await _dbContext.SaveChangesAsync(); - } -} \ No newline at end of file + protected ApplicationDbContext context = context; + + public async Task Add(T entity) + { + await context.Set().AddAsync(entity); + } + + public async Task Delete(T entity) + { + context.Set().Remove(entity); + await Task.CompletedTask; + } + + public async Task> GetAll() + { + return await context.Set().ToListAsync(); + } + + public async Task GetById(int id) + { + return await context.Set().FindAsync(id); + } + + public Task Update(T entity) + { + context.Set().Update(entity); + return Task.CompletedTask; + } + + public Task> GetAllSync() + { + return Task.FromResult((IQueryable)context.Set()); + } +} diff --git a/ContainerManagement/Infrastructure/Repositories/ContainerHealthRecordRepository.cs b/ContainerManagement/Infrastructure/Repositories/ContainerHealthRecordRepository.cs new file mode 100644 index 0000000..feaebb2 --- /dev/null +++ b/ContainerManagement/Infrastructure/Repositories/ContainerHealthRecordRepository.cs @@ -0,0 +1,26 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; + +public class ContainerHealthRecordRepository : BaseRepository, IContainerHealthRecordRepository +{ + public ContainerHealthRecordRepository(ApplicationDbContext dbContext) : base(dbContext) + { + } + + public async Task> GetLatestReportsByTime(int containerId, DateTime from, DateTime? to = null) + { + if (to == null) + { + to = DateTime.Now; + } + + return await context.ContainerHealthRecords + .Where(x => x.ContainerId == containerId && x.SavedAt >= from && x.SavedAt <= to) + .OrderByDescending(x => x.SavedAt) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/ContainerRepository.cs b/ContainerManagement/Infrastructure/Repositories/ContainerRepository.cs new file mode 100644 index 0000000..566e218 --- /dev/null +++ b/ContainerManagement/Infrastructure/Repositories/ContainerRepository.cs @@ -0,0 +1,15 @@ +using DittoBox.EdgeServer.ContainerManagement.Application.Services; +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories +{ + public class ContainerRepository(ApplicationDbContext dbContext) : BaseRepository(dbContext), IContainerRepository + { + public Task GetContainerByUIID(string uiid) + { + return context.Containers.FirstOrDefaultAsync(c => c.UIID == uiid); + } + } +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/ContainerStatusRecordRepository.cs b/ContainerManagement/Infrastructure/Repositories/ContainerStatusRecordRepository.cs new file mode 100644 index 0000000..5ee13bc --- /dev/null +++ b/ContainerManagement/Infrastructure/Repositories/ContainerStatusRecordRepository.cs @@ -0,0 +1,27 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; +using Microsoft.EntityFrameworkCore; + +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories +{ + public class ContainerStatusRecordRepository : BaseRepository, IContainerStatusRecordRepository + { + public ContainerStatusRecordRepository(ApplicationDbContext dbContext) : base(dbContext) + { + } + + public async Task> GetLatestReportsByTime(int containerId, DateTime from, DateTime? to) + { + if (to == null) + { + to = DateTime.Now; + } + + return await context.ContainerStatusRecords + .Where(x => x.ContainerId == containerId && x.SavedAt >= from && x.SavedAt <= to) + .OrderByDescending(x => x.SavedAt).ToListAsync(); + } + + + } +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/IBaseRepository.cs b/ContainerManagement/Infrastructure/Repositories/IBaseRepository.cs index 00cc00c..e1c07c6 100644 --- a/ContainerManagement/Infrastructure/Repositories/IBaseRepository.cs +++ b/ContainerManagement/Infrastructure/Repositories/IBaseRepository.cs @@ -1,11 +1,12 @@ -namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; +public interface IBaseRepository where T : class { - public interface IBaseRepository where T : class - { - Task GetByIdAsync(Guid id); - Task> GetAllAsync(); - Task AddAsync(T entity); - Task UpdateAsync(T entity); - Task DeleteAsync(T entity); - } + Task GetById(int id); + Task> GetAll(); + Task Add(T entity); + Task Update(T entity); + Task Delete(T entity); + + Task> GetAllSync(); + } \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/IContainerHealthRecordRepository.cs b/ContainerManagement/Infrastructure/Repositories/IContainerHealthRecordRepository.cs new file mode 100644 index 0000000..28dcb17 --- /dev/null +++ b/ContainerManagement/Infrastructure/Repositories/IContainerHealthRecordRepository.cs @@ -0,0 +1,10 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; + +namespace DittoBox.EdgeServer.ContainerManagement.Domain.Services +{ + public interface IContainerHealthRecordRepository : IBaseRepository + { + public Task> GetLatestReportsByTime(int containerId, DateTime from, DateTime? to = null); + } +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/IContainerRepository.cs b/ContainerManagement/Infrastructure/Repositories/IContainerRepository.cs new file mode 100644 index 0000000..f1d7213 --- /dev/null +++ b/ContainerManagement/Infrastructure/Repositories/IContainerRepository.cs @@ -0,0 +1,11 @@ + +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; + +namespace DittoBox.EdgeServer.ContainerManagement.Application.Services +{ + public interface IContainerRepository : IBaseRepository + { + public Task GetContainerByUIID(string uiid); + } +} \ No newline at end of file diff --git a/ContainerManagement/Infrastructure/Repositories/IContainerStatusRecordRepository.cs b/ContainerManagement/Infrastructure/Repositories/IContainerStatusRecordRepository.cs new file mode 100644 index 0000000..2b4754e --- /dev/null +++ b/ContainerManagement/Infrastructure/Repositories/IContainerStatusRecordRepository.cs @@ -0,0 +1,9 @@ +using DittoBox.EdgeServer.ContainerManagement.Domain.Models.Entities; + +namespace DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories +{ + public interface IContainerStatusRecordRepository : IBaseRepository + { + public Task> GetLatestReportsByTime(int containerId, DateTime from, DateTime? to = null); + } +} \ No newline at end of file diff --git a/ContainerManagement/Interface/CloudServiceController.cs b/ContainerManagement/Interface/CloudServiceController.cs index 83f5201..a6ee9e0 100644 --- a/ContainerManagement/Interface/CloudServiceController.cs +++ b/ContainerManagement/Interface/CloudServiceController.cs @@ -1,13 +1,33 @@ -using Microsoft.AspNetCore.Mvc; +using DittoBox.EdgeServer.ContainerManagement.Application; +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces; +using DittoBox.EdgeServer.ContainerManagement.Application.Resources; +using Microsoft.AspNetCore.Mvc; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 namespace DittoBox.EdgeServer.ContainerManagement.Interface { - [Route("api/[controller]")] + [Route("api/v1/cloud-service")] [ApiController] - public class CloudServiceController : ControllerBase + public class CloudServiceController( + IContainerStatusReportCommandHandler containerStatusReportCommandHandler, + IContainerSelfRegisterCommandHandler containerSelfRegisterCommandHandler, + ILogger logger + ) : ControllerBase { // Containers send requests to this endpoint to forward them to the cloud service. The edge server will analyze and decide whether to forward the request to the cloud service or aggregate it with other requests. + + // POST api/v1/cloud-service/status + [HttpPost("send-container-status")] + public async Task PostStatusAsync([FromBody] ContainerStatusReportCommand command) { + await containerStatusReportCommandHandler.Handle(command); + return Ok(); + } + + [HttpPost("self-register-container")] + public async Task> PostSelfRegisterContainerAsync([FromBody] ContainerSelfRegisterCommand command) { + var result = await containerSelfRegisterCommandHandler.Handle(command); + return Ok(result); + } } } diff --git a/ContainerManagement/Interface/ContainerController.cs b/ContainerManagement/Interface/ContainerController.cs index 9eb1136..7c3d2d0 100644 --- a/ContainerManagement/Interface/ContainerController.cs +++ b/ContainerManagement/Interface/ContainerController.cs @@ -1,13 +1,22 @@ -using Microsoft.AspNetCore.Mvc; +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces; +using Microsoft.AspNetCore.Mvc; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 namespace DittoBox.EdgeServer.ContainerManagement.Interface { - [Route("api/[controller]")] + [Route("api/v1/container")] [ApiController] - public class ContainerController : ControllerBase + public class ContainerController( + IGetContainersQueryHandler getContainersQueryHandler + ) : ControllerBase { // Cloud Service sends requests to this endpoint to forward them to the container service. The edge server will analyze and decide whether to forward the request to the container service or aggregate it with other requests. Most likely it will just forward the request to the container. + + [HttpGet] + public async Task GetRegisteredContainers() { + var response = await getContainersQueryHandler.Handle(); + return Ok(response); + } } } diff --git a/DittoBox.EdgeServer.csproj b/DittoBox.EdgeServer.csproj index 9d436fa..ca9f73c 100644 --- a/DittoBox.EdgeServer.csproj +++ b/DittoBox.EdgeServer.csproj @@ -9,6 +9,7 @@ + diff --git a/Program.cs b/Program.cs index 00ff539..33462f6 100644 --- a/Program.cs +++ b/Program.cs @@ -1,44 +1,86 @@ -var builder = WebApplication.CreateBuilder(args); +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Interfaces; +using DittoBox.EdgeServer.ContainerManagement.Application.Handlers.Internal; +using DittoBox.EdgeServer.ContainerManagement.Application.Services; +using DittoBox.EdgeServer.ContainerManagement.Domain.Services; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Configuration; +using DittoBox.EdgeServer.ContainerManagement.Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +namespace DittoBox.EdgeServer; -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) +public class Program { - app.UseSwagger(); - app.UseSwaggerUI(); -} + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); -app.UseHttpsRedirection(); + builder.Services.AddControllers(); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + builder.Configuration.AddUserSecrets(); -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; + var folder = Environment.SpecialFolder.LocalApplicationData; + var path = Environment.GetFolderPath(folder); + var sqliteConnectionString = $"Data Source={Path.Join(path, "edge-server.db")}"; -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast") -.WithOpenApi(); - -app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + if (string.IsNullOrEmpty(sqliteConnectionString)) + { + throw new Exception("SQLITE_CONNECTION_STRING environment variable not set"); + } + + builder.Services.AddDbContext(options => + { + options.UseSqlite(sqliteConnectionString); + }); + + builder.Services.AddScoped(); + + builder.Services.AddCors(options => + { + options.AddPolicy("AllowAll", corsPolicyBuilder => + { + corsPolicyBuilder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); + + ConfigureServices(builder); + + var app = builder.Build(); + app.UseSwagger(); + app.UseSwaggerUI(); + + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + if (Environment.GetEnvironmentVariable("RESET_DATABASE") == "true") { + db.Database.EnsureDeleted(); + } + db.Database.EnsureCreated(); + } + + app.UseHttpsRedirection(); + + app.UseCors("AllowAll"); + + app.MapControllers(); + + app.Run(); + + } + + private static void ConfigureServices(WebApplicationBuilder builder) + { + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + } } diff --git a/appsettings.Development.json b/appsettings.Development.json index 0c208ae..54151b6 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "None", + "Microsoft.EntityFrameworkCore": "Warning" } } } diff --git a/appsettings.json b/appsettings.json index 10f68b8..51c11b2 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Warning", "Microsoft.AspNetCore": "Warning" } },