Skip to content

Commit

Permalink
Merge pull request #5 from Team-Wilhelm/mqtt-business
Browse files Browse the repository at this point in the history
Mqtt business
  • Loading branch information
juuwel authored Apr 25, 2024
2 parents ee3a9c4 + 02564bc commit 299a543
Show file tree
Hide file tree
Showing 16 changed files with 219 additions and 22 deletions.
2 changes: 1 addition & 1 deletion Core/Options/MqttOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace api.Options;
namespace Core.Options;

public class MqttOptions
{
Expand Down
114 changes: 114 additions & 0 deletions Core/Services/ConditionsLogsService.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
32 changes: 18 additions & 14 deletions Core/Services/MqttSubscriberService.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
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<MqttOptions> _options;
private readonly ConditionsLogsService _conditionsLogService;

public MqttSubscriberService(IOptions<MqttOptions> options)
public MqttSubscriberService(IOptions<MqttOptions> 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");
}

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)
Expand All @@ -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<CreateConditionsLogDto>(payload, options:
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

if (conditions is null) return;

await _conditionsLogService.CreateConditionsLogAsync(conditions);
};

await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
Expand All @@ -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();
}
}
1 change: 1 addition & 0 deletions Core/Services/PlantService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public async Task<Plant> 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);
Expand Down
3 changes: 3 additions & 0 deletions Infrastructure/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

modelBuilder.Entity<Requirements>()
.HasKey(e => e.ConditionsId);

modelBuilder.Entity<ConditionsLog>()
.HasKey(e => e.ConditionsId);

base.OnModelCreating(modelBuilder);
}
Expand Down
11 changes: 10 additions & 1 deletion Infrastructure/Repositories/ConditionsLogsRepository.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Shared.Models.Information;

namespace Infrastructure.Repositories;

public class ConditionsLogsRepository
public class ConditionsLogsRepository (IDbContextFactory<ApplicationDbContext> dbContextFactory)
{
public async Task CreateConditionsLogAsync(ConditionsLog conditionsLog)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
await context.ConditionsLogs.AddAsync(conditionsLog);
await context.SaveChangesAsync();
}
}
29 changes: 29 additions & 0 deletions Infrastructure/Repositories/PlantRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,33 @@ public async Task DeletePlant(Plant plant)
context.Plants.Remove(plant);
await context.SaveChangesAsync();
}

public async Task<Guid> 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<Conditions> 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;
}
}
11 changes: 11 additions & 0 deletions Shared/Dtos/CreateConditionsLogDto.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
1 change: 1 addition & 0 deletions Shared/Dtos/FromClient/Plant/CreatePlantDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down
2 changes: 1 addition & 1 deletion Shared/Dtos/FromClient/Plant/UpdatePlantDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
16 changes: 16 additions & 0 deletions Shared/Models/Exceptions/RegisterDeviceExeption.cs
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
2 changes: 0 additions & 2 deletions Shared/Models/Information/ConditionsLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
2 changes: 1 addition & 1 deletion Shared/Models/Plant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
5 changes: 5 additions & 0 deletions api/Events/Global/ServerRespondsRegisterDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace api.Events.Global;

public class ServerRespondsRegisterDevice : ServerSendsErrorMessage
{
}
1 change: 1 addition & 0 deletions api/GlobalExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions api/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -48,15 +47,17 @@ public static async Task<WebApplication> StartApi(string[] args)
builder.Services.AddSingleton<UserRepository>();
builder.Services.AddSingleton<PlantRepository>();
builder.Services.AddSingleton<RequirementsRepository>();
builder.Services.AddSingleton<ConditionsLogsRepository>();
builder.Services.AddSingleton<UserService>();
builder.Services.AddSingleton<PlantService>();
builder.Services.AddSingleton<RequirementService>();
builder.Services.AddSingleton<ConditionsLogsService>();
builder.Services.AddSingleton<MqttSubscriberService>();
// 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" } };
});

Expand Down Expand Up @@ -118,6 +119,10 @@ await userRepository.CreateUser(new RegisterUserDto
}
};
});

// Connect and subscribe to MQTT
var mqttSubscriberService = app.Services.GetRequiredService<MqttSubscriberService>();
_ = mqttSubscriberService.SubscribeAsync();

return app;
}
Expand Down

0 comments on commit 299a543

Please sign in to comment.