diff --git a/Infrastructure/ApplicationDbContext.cs b/Infrastructure/ApplicationDbContext.cs index 1024629..362424d 100644 --- a/Infrastructure/ApplicationDbContext.cs +++ b/Infrastructure/ApplicationDbContext.cs @@ -54,172 +54,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - - public async Task SeedDevelopmentDataAsync(IServiceScope scope, string defaultPlantImage) - { - var userRepository = scope.ServiceProvider.GetRequiredService(); - await userRepository.CreateUser(new RegisterUserDto - { - Email = "bob@app.com", - Password = "password", - Username = "bob" - }); - - var collectionsRepository = scope.ServiceProvider.GetRequiredService(); - var collection1 = await collectionsRepository.CreateCollection( - new Collection - { - CollectionId = Guid.NewGuid(), - Name = "Succulents", - UserEmail = "bob@app.com", - } - ); - var collection2 = await collectionsRepository.CreateCollection( - new Collection - { - CollectionId = Guid.NewGuid(), - Name = "Cacti", - UserEmail = "bob@app.com", - } - ); - - var plantRepository = scope.ServiceProvider.GetRequiredService(); - await plantRepository.CreatePlant( - new Plant - { - PlantId = Guid.NewGuid(), - Nickname = "Aloe Vera", - UserEmail = "bob@app.com", - ImageUrl = defaultPlantImage, - CollectionId = collection1.CollectionId, - LatestChange = DateTime.UtcNow.Subtract(TimeSpan.FromDays(5)) - } - ); - - await plantRepository.CreatePlant( - new Plant - { - PlantId = Guid.NewGuid(), - Nickname = "Prickly Pear", - UserEmail = "bob@app.com", - ImageUrl = defaultPlantImage, - CollectionId = collection2.CollectionId, - LatestChange = DateTime.UtcNow.Subtract(TimeSpan.FromDays(7)) - } - ); - - await plantRepository.CreatePlant( - new Plant - { - PlantId = Guid.NewGuid(), - Nickname = "Dying plant", - UserEmail = "bob@app.com", - ImageUrl = defaultPlantImage, - CollectionId = collection2.CollectionId, - LatestChange = DateTime.UtcNow - } - ); - - var plants = await plantRepository.GetPlantsForUser("bob@app.com", 1, 5); - - var requirementsRepository = scope.ServiceProvider.GetRequiredService(); - await requirementsRepository.CreateRequirements( - new Requirements - { - RequirementsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Aloe Vera").PlantId, - LightLevel = RequirementLevel.Low, - SoilMoistureLevel = RequirementLevel.Medium, - HumidityLevel = RequirementLevel.High, - TemperatureLevel = 22, - } - ); - - await requirementsRepository.CreateRequirements( - new Requirements - { - RequirementsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Prickly Pear").PlantId, - LightLevel = RequirementLevel.High, - SoilMoistureLevel = RequirementLevel.Low, - HumidityLevel = RequirementLevel.Low, - TemperatureLevel = 27, - } - ); - - await requirementsRepository.CreateRequirements( - new Requirements - { - RequirementsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Dying plant").PlantId, - LightLevel = RequirementLevel.High, - SoilMoistureLevel = RequirementLevel.Low, - HumidityLevel = RequirementLevel.Medium, - TemperatureLevel = 24, - } - ); - - var conditionsLogRepository = scope.ServiceProvider.GetRequiredService(); - - for (var i = 0; i < 100; i++) - { - await conditionsLogRepository.CreateConditionsLogAsync( - GetRandomConditionsLog(plants.First(p => p.Nickname == "Prickly Pear").PlantId, i * 6) - ); - await conditionsLogRepository.CreateConditionsLogAsync( - GetRandomConditionsLog(plants.First(p => p.Nickname == "Aloe Vera").PlantId, i * 6) - ); - } - - await conditionsLogRepository.CreateConditionsLogAsync( - new ConditionsLog() - { - ConditionsId = Guid.NewGuid(), - PlantId = plants.First(p => p.Nickname == "Dying plant").PlantId, - TimeStamp = DateTime.UtcNow, - Mood = 0, - SoilMoisture = 55, - Light = 13, - Humidity = 68, - Temperature = 25, - } - ); - - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("Seeded development data"); - Console.ResetColor(); - } - - private double GetRandomLevelValue() - { - var random = new Random(); - return random.NextDouble() * 100; - } - - private int GetRandomMood() - { - var random = new Random(); - return random.Next(0, 5); - } - - private int GetRandomTemperature() - { - var random = new Random(); - return random.Next(-20, 45); - } - - private ConditionsLog GetRandomConditionsLog(Guid plantId, int hoursAgo = 0) - { - return new ConditionsLog - { - ConditionsId = Guid.NewGuid(), - PlantId = plantId, - TimeStamp = DateTime.UtcNow.Subtract(TimeSpan.FromHours(hoursAgo)), - Mood = GetRandomMood(), - SoilMoisture = GetRandomLevelValue(), - Light = GetRandomLevelValue(), - Temperature = GetRandomTemperature(), - Humidity = GetRandomLevelValue(), - }; - } } \ No newline at end of file diff --git a/Infrastructure/Repositories/PlantRepository.cs b/Infrastructure/Repositories/PlantRepository.cs index 0727ce1..116936e 100644 --- a/Infrastructure/Repositories/PlantRepository.cs +++ b/Infrastructure/Repositories/PlantRepository.cs @@ -98,7 +98,7 @@ public async Task> GetCriticalPlants(string requesterEmail) await using var context = await dbContextFactory.CreateDbContextAsync(); return await context.Plants .Include(plant => plant.Requirements) - .Include(plant => plant.ConditionsLogs) + .Include(plant => plant.ConditionsLogs.OrderByDescending(log => log.TimeStamp).Take(1)) .Where(p => p.UserEmail == requesterEmail && p.ConditionsLogs.Count != 0) .Select(p => new { @@ -114,6 +114,7 @@ public async Task> GetCriticalPlants(string requesterEmail) .Select(p => p.Plant) .ToListAsync(); } + public async Task GetStats(string userEmail) { await using var context = await dbContextFactory.CreateDbContextAsync(); diff --git a/Shared/Dtos/CreateConditionsLogDto.cs b/Shared/Dtos/CreateConditionsLogDto.cs index e34a17c..d708a54 100644 --- a/Shared/Dtos/CreateConditionsLogDto.cs +++ b/Shared/Dtos/CreateConditionsLogDto.cs @@ -6,5 +6,5 @@ public class CreateConditionsLogDto public double Light { get; set; } public double Temperature { get; set; } public double Humidity { get; set; } - public long DeviceId { get; set; } + public required string DeviceId { get; set; } } \ No newline at end of file diff --git a/api/Core/Services/MqttPublisherService.cs b/api/Core/Services/MqttPublisherService.cs index af0686b..56cd622 100644 --- a/api/Core/Services/MqttPublisherService.cs +++ b/api/Core/Services/MqttPublisherService.cs @@ -22,10 +22,9 @@ public MqttPublisherService(IOptions options) if (string.IsNullOrEmpty(_options.Value.Username) || _options.Value.Username == "FILL_ME_IN") throw new Exception("MQTT username not set in appsettings.json"); } - public async Task PublishAsync(MoodDto mood, long deviceId) + public async Task PublishAsync(MoodDto mood, string deviceId) { var mqttFactory = new MqttFactory(); - using var mqttClient = mqttFactory.CreateMqttClient(); var mqttClientOptions = new MqttClientOptionsBuilder() diff --git a/api/WebSocketConnectionService.cs b/api/Core/Services/WebSocketConnectionService.cs similarity index 98% rename from api/WebSocketConnectionService.cs rename to api/Core/Services/WebSocketConnectionService.cs index a7c1cde..af4b500 100644 --- a/api/WebSocketConnectionService.cs +++ b/api/Core/Services/WebSocketConnectionService.cs @@ -1,5 +1,4 @@ using Fleck; -using Shared; using Shared.Wrappers; namespace api; diff --git a/api/DbInitializer.cs b/api/DbInitializer.cs new file mode 100644 index 0000000..72f90cf --- /dev/null +++ b/api/DbInitializer.cs @@ -0,0 +1,246 @@ +using api.Core.Services; +using Infrastructure; +using Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using Shared.Dtos; +using Shared.Dtos.FromClient.Collections; +using Shared.Dtos.FromClient.Identity; +using Shared.Dtos.FromClient.Plant; +using Shared.Dtos.FromClient.Requirements; +using Shared.Models; +using Shared.Models.Information; + +namespace api; + +public class DbInitializer(IServiceProvider serviceProvider) +{ + private const string DefaultUserEmail = "bob@botanique.com"; + private readonly IServiceScope _scope = serviceProvider.CreateScope(); + private readonly Dictionary _collections = new(); + private readonly Dictionary _plants = new(); + + public async Task InitializeDatabaseAsync() + { + var scope = serviceProvider.CreateScope(); + var db = await scope.ServiceProvider.GetRequiredService>().CreateDbContextAsync(); + + if (EnvironmentHelper.IsNonProd()) + { + await db.Database.EnsureDeletedAsync(); + } + + await db.Database.EnsureCreatedAsync(); + await db.Database.MigrateAsync(); + } + + public async Task PopulateDatabaseAsync() + { + await CreateUser(); + await CreateCollections(); + await CreatePlants(); + await CreateHistoricalData(); + + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Database has been populated"); + Console.ResetColor(); + } + + private async Task CreateUser() + { + await _scope.ServiceProvider + .GetRequiredService() + .CreateUser(new RegisterUserDto + { + Email = DefaultUserEmail, + Password = "SuperSecretPassword123!", + Username = "Bob" + }); + } + + private async Task CreateCollections() + { + var collectionService = _scope.ServiceProvider.GetRequiredService(); + var succulents = await collectionService.CreateCollection( + new CreateCollectionDto + { + Name = "Succulents", + }, + DefaultUserEmail + ); + _collections.Add("Succulents", succulents); + + var ferns = await collectionService.CreateCollection( + new CreateCollectionDto + { + Name = "Ferns", + }, + DefaultUserEmail + ); + _collections.Add("Ferns", ferns); + } + + private List GetRequirements() + { + var createRequirementsList = new List + { + // Aloe Vera + new() + { + LightLevel = RequirementLevel.Medium, + SoilMoistureLevel = RequirementLevel.Medium, + HumidityLevel = RequirementLevel.Low, + TemperatureLevel = 22, + }, + // Prickly pear + new() + { + LightLevel = RequirementLevel.High, + SoilMoistureLevel = RequirementLevel.Low, + HumidityLevel = RequirementLevel.Low, + TemperatureLevel = 27, + }, + // Dying plant + new() + { + LightLevel = RequirementLevel.Low, + SoilMoistureLevel = RequirementLevel.High, + HumidityLevel = RequirementLevel.Medium, + TemperatureLevel = 20, + } + }; + + return createRequirementsList; + } + + private async Task CreatePlants() + { + var requirements = GetRequirements(); + + var plantService = _scope.ServiceProvider.GetRequiredService(); + var plant1 = await plantService.CreatePlant( + new CreatePlantDto + { + Nickname = "Aloe Vera", + CreateRequirementsDto = requirements[0], + DeviceId = "264625477326660", + }, DefaultUserEmail + ); + _plants.Add("Aloe Vera", plant1); + + var plant2 = await plantService.CreatePlant( + new CreatePlantDto + { + Nickname = "Prickly Pear", + CreateRequirementsDto = requirements[1], + CollectionId = _collections["Succulents"].CollectionId, + DeviceId = "000000000000001" + }, DefaultUserEmail + ); + _plants.Add("Prickly Pear", plant2); + + var plant3 = await plantService.CreatePlant( + new CreatePlantDto + { + Nickname = "Dying plant", + CollectionId = _collections["Ferns"].CollectionId, + CreateRequirementsDto = requirements[2], + DeviceId = "000000000000002" + }, DefaultUserEmail + ); + _plants.Add("Dying plant", plant3); + } + + private async Task CreateHistoricalData() + { + // Create 100 logs for each plant + var conditionsLogService = _scope.ServiceProvider.GetRequiredService(); + var aloeVeraRequirements = _plants["Aloe Vera"].Requirements!; + var pricklyPearRequirements = _plants["Prickly Pear"].Requirements!; + + for (var i = 0; i < 100; i++) + { + await conditionsLogService.CreateConditionsLogAsync( + new CreateConditionsLogDto + { + DeviceId = _plants["Aloe Vera"].DeviceId!, + SoilMoisturePercentage = GetValueNearOrInIdealRange(aloeVeraRequirements.SoilMoistureLevel), + Light = GetValueNearOrInIdealRange(aloeVeraRequirements.LightLevel), + Temperature = GetRandomTemperature(), + Humidity = GetValueNearOrInIdealRange(aloeVeraRequirements.HumidityLevel), + } + ); + await conditionsLogService.CreateConditionsLogAsync( + new CreateConditionsLogDto + { + DeviceId = _plants["Prickly Pear"].DeviceId!, + SoilMoisturePercentage = GetValueNearOrInIdealRange(pricklyPearRequirements.SoilMoistureLevel), + Light = GetValueNearOrInIdealRange(pricklyPearRequirements.LightLevel), + Temperature = GetRandomTemperature(), + Humidity = GetValueNearOrInIdealRange(pricklyPearRequirements.HumidityLevel), + } + ); + } + + // Adjust the timestamps to be in past + var db = await _scope.ServiceProvider.GetRequiredService>().CreateDbContextAsync(); + var logs = await db.ConditionsLogs.ToListAsync(); + for (var i = 0; i < 100; i++) + { + logs[i].TimeStamp = logs[i].TimeStamp.AddDays(-i); + logs[i + 100].TimeStamp = logs[i + 100].TimeStamp.AddDays(-i); + } + db.UpdateRange(logs); + await db.SaveChangesAsync(); + + var dyingPlant = _plants["Dying plant"]; + await conditionsLogService.CreateConditionsLogAsync( + + new CreateConditionsLogDto + { + DeviceId = dyingPlant.DeviceId!, + SoilMoisturePercentage = GetValueOutsideOfIdealRange(dyingPlant.Requirements!.SoilMoistureLevel), + Light = GetValueOutsideOfIdealRange(dyingPlant.Requirements!.LightLevel), + Temperature = dyingPlant.Requirements!.TemperatureLevel - 10, + Humidity = GetValueOutsideOfIdealRange(dyingPlant.Requirements!.HumidityLevel), + } + ); + } + + private double GetRandomLevelValue() + { + var random = new Random(); + return random.NextDouble() * 100; + } + + private int GetRandomTemperature() + { + var random = new Random(); + return random.Next(-20, 45); + } + + private double GetValueNearOrInIdealRange(RequirementLevel level) + { + var idealValueRange = level.GetRange(); + var random = new Random(); + var value = random.NextDouble() * 100; + while (value < idealValueRange.Min - 10 || value > idealValueRange.Max + 10) + { + value = random.NextDouble() * 100; + } + + return value; + } + + private double GetValueOutsideOfIdealRange(RequirementLevel level) + { + var idealValueRange = level.GetRange(); + var random = new Random(); + var value = random.NextDouble() * 100; + while (value > idealValueRange.Min && value < idealValueRange.Max) + { + value = random.NextDouble() * 100; + } + + return value; + } +} \ No newline at end of file diff --git a/api/Extensions/ConfigureExtensions.cs b/api/Extensions/ConfigureExtensions.cs index 43bfcd2..9f64ec4 100644 --- a/api/Extensions/ConfigureExtensions.cs +++ b/api/Extensions/ConfigureExtensions.cs @@ -26,7 +26,14 @@ public static void ConfigureOptions(this WebApplicationBuilder builder) options.PublishTopic = Environment.GetEnvironmentVariable("MQTT_PUBLISH_TOPIC") ?? throw new Exception("MQTT publish topic is missing"); }); - if (EnvironmentHelper.IsTesting()) return; + if (EnvironmentHelper.IsTesting()) + { + builder.Services.Configure(options => + { + options.DefaultPlantImageUrl = "https://example.com"; + }); + return; + } builder.Services.Configure(options => { diff --git a/api/Program.cs b/api/Program.cs index d3df78d..33f6cda 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -60,13 +60,12 @@ public static async Task StartApi(string[] args) await dbContainer.StartAsync(); - var connectionString = dbContainer.GetConnectionString(); + var connectionString = dbContainer.GetConnectionString() + ";Include Error Detail=true"; builder.Services.AddDbContextFactory(options => { options.UseNpgsql(connectionString ?? throw new Exception("Connection string cannot be null")); }); } - else { var connectionString = builder.Configuration.GetConnectionString("BotaniqueDb"); @@ -84,23 +83,12 @@ public static async Task StartApi(string[] args) var app = builder.Build(); + // be careful with using --db-init on production, it will delete all data if (args.Contains("--db-init")) { - var scope = app.Services.CreateScope(); - var db = await app.Services.GetRequiredService>().CreateDbContextAsync(); - - if (EnvironmentHelper.IsNonProd()) - { - await db.Database.EnsureDeletedAsync(); - } - - await db.Database.EnsureCreatedAsync(); - await db.Database.MigrateAsync(); - - if (EnvironmentHelper.IsNonProd()) - { - await db.SeedDevelopmentDataAsync(scope, app.Configuration["AzureBlob:DefaultPlantImageUrl"] ?? "https://example.com"); - } + var dbInitializer = new DbInitializer(app.Services); + await dbInitializer.InitializeDatabaseAsync(); + await dbInitializer.PopulateDatabaseAsync(); } builder.WebHost.UseUrls("http://*:9999");