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/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 91e42ed..98fb991 100644 --- a/Core/Services/MqttSubscriberService.cs +++ b/Core/Services/MqttSubscriberService.cs @@ -1,17 +1,22 @@ -using api.Options; +using System.Text; +using System.Text.Json; +using Core.Options; using Microsoft.Extensions.Options; using MQTTnet; using MQTTnet.Client; +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"); @@ -19,13 +24,8 @@ public MqttSubscriberService(IOptions options) public async Task SubscribeAsync() { - /* - * This sample subscribes to a topic and processes the received message. - */ - var mqttFactory = new MqttFactory(); - - //TODO: remove token before pushing to GitHub + using var mqttClient = mqttFactory.CreateMqttClient(); var mqttClientOptions = new MqttClientOptionsBuilder() .WithTcpServer(_options.Value.Server, _options.Value.Port) @@ -35,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); @@ -49,9 +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(); } } \ 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 8397c94..691004e 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; @@ -48,15 +47,17 @@ 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 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 +119,10 @@ await userRepository.CreateUser(new RegisterUserDto } }; }); + + // Connect and subscribe to MQTT + var mqttSubscriberService = app.Services.GetRequiredService(); + _ = mqttSubscriberService.SubscribeAsync(); return app; }