From 681cec8e8c7c3ee819a190b7b615b3de871b3eab Mon Sep 17 00:00:00 2001 From: mariaruth1 <113031776+mariaruth1@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:10:33 +0200 Subject: [PATCH 1/2] stuffs --- Core/Options/MqttOptions.cs | 2 +- Core/Services/MqttSubscriberService.cs | 14 ++++++++------ api/Program.cs | 7 +++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Core/Options/MqttOptions.cs b/Core/Options/MqttOptions.cs index 257eb9c..44f5199 100644 --- a/Core/Options/MqttOptions.cs +++ b/Core/Options/MqttOptions.cs @@ -1,4 +1,4 @@ -namespace api.Options; +namespace Core.Options; public class MqttOptions { diff --git a/Core/Services/MqttSubscriberService.cs b/Core/Services/MqttSubscriberService.cs index 91e42ed..cbc9ecb 100644 --- a/Core/Services/MqttSubscriberService.cs +++ b/Core/Services/MqttSubscriberService.cs @@ -1,7 +1,8 @@ -using api.Options; +using Core.Options; using Microsoft.Extensions.Options; using MQTTnet; using MQTTnet.Client; +using Shared.Models.Information; namespace Core.Services; @@ -19,11 +20,7 @@ public MqttSubscriberService(IOptions options) public async Task SubscribeAsync() { - /* - * This sample subscribes to a topic and processes the received message. - */ - - var mqttFactory = new MqttFactory(); + var mqttFactory = new MqttFactory(); //TODO: remove token before pushing to GitHub using var mqttClient = mqttFactory.CreateMqttClient(); @@ -54,4 +51,9 @@ public async Task SubscribeAsync() Console.WriteLine("Press enter to exit."); Console.ReadLine(); } + + private async Task SaveConditionsAsync(Guid plantId, ConditionsLog conditions) + { + + } } \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index 8397c94..9b956f0 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -1,6 +1,5 @@ using System.Reflection; using System.Text.Json; -using api.Options; using AsyncApi.Net.Generator; using AsyncApi.Net.Generator.AsyncApiSchema.v2; using Core.Options; @@ -56,7 +55,7 @@ public static async Task StartApi(string[] args) builder.Services.AddAsyncApiSchemaGeneration(o => { - o.AssemblyMarkerTypes = new[] { typeof(BaseDto) }; // add assemply marker + o.AssemblyMarkerTypes = new[] { typeof(BaseDto) }; // add assembly marker o.AsyncApi = new AsyncApiDocument { Info = new Info { Title = "BotaniQue" } }; }); @@ -118,6 +117,10 @@ await userRepository.CreateUser(new RegisterUserDto } }; }); + + // Connect and subscribe to MQTT + var mqttSubscriberService = app.Services.GetRequiredService(); + _ = mqttSubscriberService.SubscribeAsync(); return app; } From 7debc1ded611fe5b300b362265926ca8380908a1 Mon Sep 17 00:00:00 2001 From: mariaruth1 <113031776+mariaruth1@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:59:31 +0200 Subject: [PATCH 2/2] connection to mqtt sorted as well as saving conditionslogs in db, introduced device id in order to link plant in mobile app to iot device, introduced mood calculation, however this needs to have real values based on the sensor calibration. Made changes to database models so you should run --db-init --- Core/Services/ConditionsLogsService.cs | 114 ++++++++++++++++++ Core/Services/MqttSubscriberService.cs | 34 +++--- Core/Services/PlantService.cs | 1 + Infrastructure/ApplicationDbContext.cs | 3 + .../Repositories/ConditionsLogsRepository.cs | 11 +- .../Repositories/PlantRepository.cs | 29 +++++ Shared/Dtos/CreateConditionsLogDto.cs | 11 ++ .../Dtos/FromClient/Plant/CreatePlantDto.cs | 1 + .../Dtos/FromClient/Plant/UpdatePlantDto.cs | 2 +- .../Exceptions/RegisterDeviceExeption.cs | 16 +++ Shared/Models/Information/ConditionsLog.cs | 2 - Shared/Models/Plant.cs | 2 +- .../Global/ServerRespondsRegisterDevice.cs | 5 + api/GlobalExceptionHandler.cs | 1 + api/Program.cs | 2 + 15 files changed, 213 insertions(+), 21 deletions(-) create mode 100644 Core/Services/ConditionsLogsService.cs create mode 100644 Shared/Dtos/CreateConditionsLogDto.cs create mode 100644 Shared/Models/Exceptions/RegisterDeviceExeption.cs create mode 100644 api/Events/Global/ServerRespondsRegisterDevice.cs diff --git a/Core/Services/ConditionsLogsService.cs b/Core/Services/ConditionsLogsService.cs new file mode 100644 index 0000000..55ef189 --- /dev/null +++ b/Core/Services/ConditionsLogsService.cs @@ -0,0 +1,114 @@ +using Infrastructure.Repositories; +using Shared.Dtos; +using Shared.Models.Exceptions; +using Shared.Models.Information; + +namespace Core.Services; + +public class ConditionsLogsService (ConditionsLogsRepository conditionsLogsRepository, PlantRepository plantRepository) +{ + public async Task CreateConditionsLogAsync(CreateConditionsLogDto createConditionsLogDto) + { + var plantId = await plantRepository.GetPlantIdByDeviceIdAsync(createConditionsLogDto.DeviceId.ToString()); + + if (plantId == Guid.Empty) + { + throw new RegisterDeviceException(); + } + var conditionsLog = new ConditionsLog + { + ConditionsId = new Guid(), + TimeStamp = DateTime.UtcNow, //TODO get this from the right place + SoilMoisture = CalculateSoilMoistureLevel(createConditionsLogDto.SoilMoisturePercentage), + LightLevel = CalculateLightLevel(createConditionsLogDto.LightLevel), + Temperature = CalculateTemperatureLevel(createConditionsLogDto.Temperature), + Humidity = CalculateHumidityLevel(createConditionsLogDto.Humidity), + PlantId = plantId + }; + + conditionsLog.Mood = CalculateMood(conditionsLog); + + Console.WriteLine("Conditions log created"); + Console.WriteLine(conditionsLog); + + await conditionsLogsRepository.CreateConditionsLogAsync(conditionsLog); + } + + private RequirementLevel CalculateTemperatureLevel (double value) + { + return value switch + { + //TODO fix these values + < 1 => RequirementLevel.Low, + > 2 => RequirementLevel.High, + _ => RequirementLevel.Medium + }; + } + + private RequirementLevel CalculateLightLevel (double value) + { + return value switch + { + //TODO fix these values + < 1 => RequirementLevel.Low, + > 2 => RequirementLevel.High, + _ => RequirementLevel.Medium + }; + } + + private RequirementLevel CalculateSoilMoistureLevel (double value) + { + return value switch + { + //TODO fix these values + < 1 => RequirementLevel.Low, + > 2 => RequirementLevel.High, + _ => RequirementLevel.Medium + }; + } + + private RequirementLevel CalculateHumidityLevel (double value) + { + return value switch + { + //TODO fix these values + < 1 => RequirementLevel.Low, + > 2 => RequirementLevel.High, + _ => RequirementLevel.Medium + }; + } + + private int CalculateMood (Conditions conditions) + { + // Compare ideal requirements for humidity, temperature, soil moisture and light level with actual conditions and calculate mood from 0-4 + // get ideal requirements from plant + var requirementsForPlant = plantRepository.GetRequirementsForPlant(conditions.PlantId); + // compare with actual conditions + var mood = 0; + // calculate mood + mood += CalculateScore((int)requirementsForPlant.Result.Humidity, (int)conditions.Humidity); + mood += CalculateScore((int)requirementsForPlant.Result.Temperature, (int)conditions.Temperature); + mood += CalculateScore((int)requirementsForPlant.Result.SoilMoisture, (int)conditions.SoilMoisture); + mood += CalculateScore((int)requirementsForPlant.Result.LightLevel, (int)conditions.LightLevel); + + if (mood == 0) + { + return 0; + } + return mood / 4; + } + + private int CalculateScore(int ideal, int actual) + { + var difference = Math.Abs(ideal - actual); + switch (difference) + { + case 0: + return 4; // Exact match + case 1: + return 2; // One away + default: + return 0; // Two away + } + } +} \ No newline at end of file diff --git a/Core/Services/MqttSubscriberService.cs b/Core/Services/MqttSubscriberService.cs index cbc9ecb..98fb991 100644 --- a/Core/Services/MqttSubscriberService.cs +++ b/Core/Services/MqttSubscriberService.cs @@ -1,18 +1,22 @@ +using System.Text; +using System.Text.Json; using Core.Options; using Microsoft.Extensions.Options; using MQTTnet; using MQTTnet.Client; -using Shared.Models.Information; +using Shared.Dtos; namespace Core.Services; public class MqttSubscriberService { private readonly IOptions _options; + private readonly ConditionsLogsService _conditionsLogService; - public MqttSubscriberService(IOptions options) + public MqttSubscriberService(IOptions options, ConditionsLogsService conditionsLogService) { _options = options; + _conditionsLogService = conditionsLogService; if (string.IsNullOrEmpty(_options.Value.Username) || _options.Value.Username == "FILL_ME_IN") throw new Exception("MQTT username not set in appsettings.json"); @@ -20,9 +24,8 @@ public MqttSubscriberService(IOptions options) public async Task SubscribeAsync() { - var mqttFactory = new MqttFactory(); - - //TODO: remove token before pushing to GitHub + var mqttFactory = new MqttFactory(); + using var mqttClient = mqttFactory.CreateMqttClient(); var mqttClientOptions = new MqttClientOptionsBuilder() .WithTcpServer(_options.Value.Server, _options.Value.Port) @@ -32,10 +35,16 @@ public async Task SubscribeAsync() // Setup message handling before connecting so that queued messages // are also handled properly. When there is no event handler attached all // received messages get lost. - mqttClient.ApplicationMessageReceivedAsync += e => + mqttClient.ApplicationMessageReceivedAsync += async e => { - // TODO: do something - return Task.CompletedTask; + //var payload = e.ApplicationMessage.ConvertPayloadToString(); + var payload = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment); + var conditions = JsonSerializer.Deserialize(payload, options: + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (conditions is null) return; + + await _conditionsLogService.CreateConditionsLogAsync(conditions); }; await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None); @@ -46,14 +55,7 @@ public async Task SubscribeAsync() .Build(); await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None); - Console.WriteLine("MQTT client subscribed to topic."); - - Console.WriteLine("Press enter to exit."); - Console.ReadLine(); - } - - private async Task SaveConditionsAsync(Guid plantId, ConditionsLog conditions) - { + Console.ReadLine(); } } \ No newline at end of file diff --git a/Core/Services/PlantService.cs b/Core/Services/PlantService.cs index bf3d083..6e97600 100644 --- a/Core/Services/PlantService.cs +++ b/Core/Services/PlantService.cs @@ -27,6 +27,7 @@ public async Task CreatePlant(CreatePlantDto createPlantDto) // CollectionId = Guid.Empty, // TODO: fix when collections are implemented Nickname = createPlantDto.Nickname, ImageUrl = createPlantDto.ImageUrl ?? DefaultImageUrl, + DeviceId = createPlantDto.DeviceId }; await plantRepository.CreatePlant(plant); diff --git a/Infrastructure/ApplicationDbContext.cs b/Infrastructure/ApplicationDbContext.cs index f4b7bef..4d83f2e 100644 --- a/Infrastructure/ApplicationDbContext.cs +++ b/Infrastructure/ApplicationDbContext.cs @@ -45,6 +45,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasKey(e => e.ConditionsId); + + modelBuilder.Entity() + .HasKey(e => e.ConditionsId); base.OnModelCreating(modelBuilder); } diff --git a/Infrastructure/Repositories/ConditionsLogsRepository.cs b/Infrastructure/Repositories/ConditionsLogsRepository.cs index de35939..757dfc2 100644 --- a/Infrastructure/Repositories/ConditionsLogsRepository.cs +++ b/Infrastructure/Repositories/ConditionsLogsRepository.cs @@ -1,5 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Shared.Models.Information; + namespace Infrastructure.Repositories; -public class ConditionsLogsRepository +public class ConditionsLogsRepository (IDbContextFactory dbContextFactory) { + public async Task CreateConditionsLogAsync(ConditionsLog conditionsLog) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + await context.ConditionsLogs.AddAsync(conditionsLog); + await context.SaveChangesAsync(); + } } \ No newline at end of file diff --git a/Infrastructure/Repositories/PlantRepository.cs b/Infrastructure/Repositories/PlantRepository.cs index da1b52d..71e3ba1 100644 --- a/Infrastructure/Repositories/PlantRepository.cs +++ b/Infrastructure/Repositories/PlantRepository.cs @@ -52,4 +52,33 @@ public async Task DeletePlant(Plant plant) context.Plants.Remove(plant); await context.SaveChangesAsync(); } + + public async Task GetPlantIdByDeviceIdAsync(string deviceId) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var plant = await context.Plants + .FirstOrDefaultAsync(p => p.DeviceId == deviceId); + + if (plant is null) + { + throw new NotFoundException($"Plant with device id {deviceId} not found"); + } + + return plant.PlantId; + } + + public async Task GetRequirementsForPlant(Guid plantId) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var plant = await context.Plants + .Include(plant => plant.Requirements) + .FirstOrDefaultAsync(p => p.PlantId == plantId); + + if (plant?.Requirements is null) + { + throw new NotFoundException($"Requirements for plant: {plantId} not found"); + } + + return plant.Requirements; + } } \ No newline at end of file diff --git a/Shared/Dtos/CreateConditionsLogDto.cs b/Shared/Dtos/CreateConditionsLogDto.cs new file mode 100644 index 0000000..ac9cc64 --- /dev/null +++ b/Shared/Dtos/CreateConditionsLogDto.cs @@ -0,0 +1,11 @@ +namespace Shared.Dtos; + +public class CreateConditionsLogDto +{ + public DateTime TimeStamp { get; set; } + public double SoilMoisturePercentage { get; set; } + public double LightLevel { get; set; } + public double Temperature { get; set; } + public double Humidity { get; set; } + public long DeviceId { get; set; } +} \ No newline at end of file diff --git a/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs b/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs index 7d1b027..2992fbf 100644 --- a/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs +++ b/Shared/Dtos/FromClient/Plant/CreatePlantDto.cs @@ -7,6 +7,7 @@ public class CreatePlantDto { [EmailAddress] public string UserEmail { get; set; } = null!; public Guid? CollectionId { get; set; } + public string? DeviceId { get; set; } [MaxLength(50)] public string? Nickname { get; set; } public string? ImageUrl { get; set; } public CreateRequirementsDto CreateRequirementsDto { get; set; } = null!; diff --git a/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs b/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs index 985c82e..ed8fb2f 100644 --- a/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs +++ b/Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs @@ -8,7 +8,7 @@ public class UpdatePlantDto { [Required] public Guid PlantId { get; set; } public Guid? CollectionId { get; set; } - + public string? DeviceId { get; set; } [MaxLength(50)] public string? Nickname { get; set; } public string? ImageUrl { get; set; } public UpdateRequirementDto? UpdateRequirementDto { get; set; } diff --git a/Shared/Models/Exceptions/RegisterDeviceExeption.cs b/Shared/Models/Exceptions/RegisterDeviceExeption.cs new file mode 100644 index 0000000..81a9f00 --- /dev/null +++ b/Shared/Models/Exceptions/RegisterDeviceExeption.cs @@ -0,0 +1,16 @@ +namespace Shared.Models.Exceptions; + +public class RegisterDeviceException: AppException +{ + public RegisterDeviceException() : base("Please provide a device ID to the relevant plant.") + { + } + + public RegisterDeviceException(string message) : base(message) + { + } + + public RegisterDeviceException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/Shared/Models/Information/ConditionsLog.cs b/Shared/Models/Information/ConditionsLog.cs index 6452e42..c7a8eaf 100644 --- a/Shared/Models/Information/ConditionsLog.cs +++ b/Shared/Models/Information/ConditionsLog.cs @@ -2,8 +2,6 @@ namespace Shared.Models.Information; public class ConditionsLog : Conditions { - public Guid ConditionsLogId { get; set; } public DateTime TimeStamp { get; set; } - public Guid PlantId { get; set; } public int Mood { get; set; } } \ No newline at end of file diff --git a/Shared/Models/Plant.cs b/Shared/Models/Plant.cs index e760225..9ba1c31 100644 --- a/Shared/Models/Plant.cs +++ b/Shared/Models/Plant.cs @@ -5,9 +5,9 @@ namespace Shared.Models; public class Plant { public Guid PlantId { get; set; } + public string? DeviceId { get; set; } public string UserEmail { get; set; } public Guid? CollectionId { get; set; } - public string? Nickname { get; set; } // if not provided make one up public string ImageUrl { get; set; } = null!; public Requirements? Requirements { get; set; } diff --git a/api/Events/Global/ServerRespondsRegisterDevice.cs b/api/Events/Global/ServerRespondsRegisterDevice.cs new file mode 100644 index 0000000..5a72176 --- /dev/null +++ b/api/Events/Global/ServerRespondsRegisterDevice.cs @@ -0,0 +1,5 @@ +namespace api.Events.Global; + +public class ServerRespondsRegisterDevice : ServerSendsErrorMessage +{ +} \ No newline at end of file diff --git a/api/GlobalExceptionHandler.cs b/api/GlobalExceptionHandler.cs index fa85279..d289453 100644 --- a/api/GlobalExceptionHandler.cs +++ b/api/GlobalExceptionHandler.cs @@ -21,6 +21,7 @@ public static void Handle(this Exception ex, IWebSocketConnection socket, string ModelValidationException => new ServerRespondsValidationError { Error = ex. Message }, NotFoundException => new ServerRespondsNotFound { Error = ex.Message }, NoAccessException => new ServerRespondsNotAuthorized { Error = ex.Message }, + RegisterDeviceException => new ServerRespondsRegisterDevice { Error = ex.Message }, _ => new ServerSendsErrorMessage { Error = message ?? ex.Message } }; else diff --git a/api/Program.cs b/api/Program.cs index 9b956f0..691004e 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -47,9 +47,11 @@ public static async Task StartApi(string[] args) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); // TODO: add repositories