diff --git a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs index 728730afb..fa0124da7 100644 --- a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs @@ -21,5 +21,7 @@ public interface ITeslaSolarChargerContext DbSet RestValueConfigurations { get; set; } DbSet RestValueConfigurationHeaders { get; set; } DbSet RestValueResultConfigurations { get; set; } + DbSet ChargingProcesses { get; set; } + DbSet ChargingDetails { get; set; } void RejectChanges(); } diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs index 8844e1099..7d9a2d4d4 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs @@ -40,4 +40,6 @@ public class Car public double? Latitude { get; set; } public double? Longitude { get; set; } public CarStateEnum? State { get; set; } + + public List ChargingProcesses { get; set; } = new List(); } diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/ChargingDetail.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/ChargingDetail.cs new file mode 100644 index 000000000..ede04afcd --- /dev/null +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/ChargingDetail.cs @@ -0,0 +1,13 @@ +namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; + +public class ChargingDetail +{ + public int Id { get; set; } + public DateTime TimeStamp { get; set; } + public int SolarPower { get; set; } + public int GridPower { get; set; } + + public int ChargingProcessId { get; set; } + + public ChargingProcess ChargingProcess { get; set; } = null!; +} diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/ChargingProcess.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/ChargingProcess.cs new file mode 100644 index 000000000..2fd9a5cfc --- /dev/null +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/ChargingProcess.cs @@ -0,0 +1,17 @@ +namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; + +public class ChargingProcess +{ + public int Id { get; set; } + public DateTime StartDate { get; set; } + public DateTime? EndDate { get; set; } + public decimal? UsedGridEnergy { get; set; } + public decimal? UsedSolarEnergy { get; set; } + public decimal? Cost { get; set; } + + public int CarId { get; set; } + + public Car Car { get; set; } = null!; + + public List ChargingDetails { get; set; } +} diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index cb0df54f1..44da7fce7 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -18,6 +18,8 @@ public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext public DbSet RestValueConfigurations { get; set; } = null!; public DbSet RestValueConfigurationHeaders { get; set; } = null!; public DbSet RestValueResultConfigurations { get; set; } = null!; + public DbSet ChargingProcesses { get; set; } = null!; + public DbSet ChargingDetails { get; set; } = null!; // ReSharper disable once UnassignedGetOnlyAutoProperty public string DbPath { get; } diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index b3c8e2234..7d88a3ca2 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -116,7 +116,7 @@ } var jobManager = app.Services.GetRequiredService(); - if (!Debugger.IsAttached) + //if (!Debugger.IsAttached) { await jobManager.StartJobs().ConfigureAwait(false); } diff --git a/TeslaSolarCharger/Server/Scheduling/JobManager.cs b/TeslaSolarCharger/Server/Scheduling/JobManager.cs index 2ad2bfc38..89ead77ca 100644 --- a/TeslaSolarCharger/Server/Scheduling/JobManager.cs +++ b/TeslaSolarCharger/Server/Scheduling/JobManager.cs @@ -50,7 +50,7 @@ public async Task StartJobs() var chargingValueJob = JobBuilder.Create().Build(); var carStateCachingJob = JobBuilder.Create().Build(); var pvValueJob = JobBuilder.Create().Build(); - var powerDistributionAddJob = JobBuilder.Create().Build(); + var chargingDetailsAddJob = JobBuilder.Create().Build(); var handledChargeFinalizingJob = JobBuilder.Create().Build(); var mqttReconnectionJob = JobBuilder.Create().Build(); var newVersionCheckJob = JobBuilder.Create().Build(); @@ -81,8 +81,8 @@ public async Task StartJobs() var carStateCachingTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(3)).Build(); - var powerDistributionAddTrigger = TriggerBuilder.Create() - .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(16)).Build(); + var chargingDetailsAddTrigger = TriggerBuilder.Create() + .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(59)).Build(); var handledChargeFinalizingTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(9)).Build(); @@ -107,7 +107,7 @@ public async Task StartJobs() {chargingValueJob, new HashSet { chargingValueTrigger }}, {carStateCachingJob, new HashSet {carStateCachingTrigger}}, {pvValueJob, new HashSet {pvValueTrigger}}, - {powerDistributionAddJob, new HashSet {powerDistributionAddTrigger}}, + {chargingDetailsAddJob, new HashSet {chargingDetailsAddTrigger}}, {handledChargeFinalizingJob, new HashSet {handledChargeFinalizingTrigger}}, {mqttReconnectionJob, new HashSet {mqttReconnectionTrigger}}, {newVersionCheckJob, new HashSet {newVersionCheckTrigger}}, diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/ChargingDetailsAddJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/ChargingDetailsAddJob.cs new file mode 100644 index 000000000..329b1bfc4 --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/ChargingDetailsAddJob.cs @@ -0,0 +1,16 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.ApiServices.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +[DisallowConcurrentExecution] +public class ChargingDetailsAddJob(ILogger logger, + ITscOnlyChargingCostService service) + : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogTrace("{method}({context})", nameof(Execute), context); + await service.AddChargingDetailsForAllCars().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/PowerDistributionAddJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/PowerDistributionAddJob.cs deleted file mode 100644 index 0d6365f6f..000000000 --- a/TeslaSolarCharger/Server/Scheduling/Jobs/PowerDistributionAddJob.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Quartz; -using TeslaSolarCharger.Server.Contracts; - -namespace TeslaSolarCharger.Server.Scheduling.Jobs; - -[DisallowConcurrentExecution] -public class PowerDistributionAddJob : IJob -{ - private readonly ILogger _logger; - private readonly IChargingCostService _service; - - public PowerDistributionAddJob(ILogger logger, IChargingCostService service) - { - _logger = logger; - _service = service; - } - public async Task Execute(IJobExecutionContext context) - { - _logger.LogTrace("{method}({context})", nameof(Execute), context); - await _service.AddPowerDistributionForAllChargingCars().ConfigureAwait(false); - } -} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index ed1eb7f07..bd0608ad0 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -40,7 +40,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() - .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() @@ -97,6 +97,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSharedBackendDependencies(); if (useFleetApi) { diff --git a/TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITscOnlyChargingCostService.cs b/TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITscOnlyChargingCostService.cs new file mode 100644 index 000000000..1afe0f5cd --- /dev/null +++ b/TeslaSolarCharger/Server/Services/ApiServices/Contracts/ITscOnlyChargingCostService.cs @@ -0,0 +1,6 @@ +namespace TeslaSolarCharger.Server.Services.ApiServices.Contracts; + +public interface ITscOnlyChargingCostService +{ + Task AddChargingDetailsForAllCars(); +} diff --git a/TeslaSolarCharger/Server/Services/ChargingCostService.cs b/TeslaSolarCharger/Server/Services/ChargingCostService.cs index e86306232..27d057cbf 100644 --- a/TeslaSolarCharger/Server/Services/ChargingCostService.cs +++ b/TeslaSolarCharger/Server/Services/ChargingCostService.cs @@ -3,10 +3,8 @@ using TeslaSolarCharger.GridPriceProvider.Data; using TeslaSolarCharger.GridPriceProvider.Services.Interfaces; using TeslaSolarCharger.Model.Contracts; -using TeslaSolarCharger.Model.Entities.TeslaMate; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Server.Contracts; -using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos.ChargingCost; using TeslaSolarCharger.Shared.Dtos.ChargingCost.CostConfigurations; diff --git a/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs b/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs new file mode 100644 index 000000000..12640da66 --- /dev/null +++ b/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs @@ -0,0 +1,152 @@ +using Microsoft.EntityFrameworkCore; +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Server.Services.ApiServices.Contracts; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Dtos.Contracts; + +namespace TeslaSolarCharger.Server.Services; + +public class TscOnlyChargingCostService(ILogger logger, + ITeslaSolarChargerContext context, + ISettings settings, + IDateTimeProvider dateTimeProvider, + IConfigurationWrapper configurationWrapper) : ITscOnlyChargingCostService +{ + public async Task FinalizeFinishedChargingProcesses() + { + logger.LogTrace("{method}()", nameof(FinalizeFinishedChargingProcesses)); + var openChargingProcesses = await context.ChargingProcesses + .Where(cp => cp.EndDate == null) + .ToListAsync().ConfigureAwait(false); + var timeSpanToHandleChargingProcessAsCompleted = TimeSpan.FromMinutes(10); + foreach (var chargingProcess in openChargingProcesses) + { + var latestChargingDetail = await context.ChargingDetails + .Where(cd => cd.ChargingProcessId == chargingProcess.Id) + .OrderByDescending(cd => cd.Id) + .FirstOrDefaultAsync().ConfigureAwait(false); + if (latestChargingDetail == default) + { + logger.LogWarning("No charging detail found for charging process with ID {chargingProcessId}.", chargingProcess.Id); + continue; + } + + if (latestChargingDetail.TimeStamp.Add(timeSpanToHandleChargingProcessAsCompleted) < dateTimeProvider.UtcNow()) + { + try + { + await FinalizeChargingProcess(chargingProcess); + } + catch (Exception ex) + { + logger.LogError(ex, "Error while finalizing charging process with ID {chargingProcessId}.", chargingProcess.Id); + } + } + } + } + + private async Task FinalizeChargingProcess(ChargingProcess chargingProcess) + { + logger.LogTrace("{method}({chargingProcessId})", nameof(FinalizeChargingProcess), chargingProcess.Id); + var chargingDetails = await context.ChargingDetails + .Where(cd => cd.ChargingProcessId == chargingProcess.Id) + .OrderBy(cd => cd.Id) + .ToListAsync().ConfigureAwait(false); + decimal usedSolarEnergy = 0; + decimal usedGridEnergy = 0; + decimal cost = 0; + for (var index = 1; index < chargingDetails.Count; index++) + { + var chargingDetail = chargingDetails[index]; + var timeSpanSinceLastDetail = chargingDetail.TimeStamp - chargingDetails[index - 1].TimeStamp; + usedSolarEnergy += (decimal)(chargingDetail.SolarPower * timeSpanSinceLastDetail.TotalHours); + usedGridEnergy += (decimal)(chargingDetail.GridPower * timeSpanSinceLastDetail.TotalHours); + } + chargingProcess.EndDate = chargingDetails.Last().TimeStamp; + chargingProcess.UsedSolarEnergy = usedSolarEnergy; + chargingProcess.UsedGridEnergy = usedGridEnergy; + chargingProcess.Cost = cost; + await context.SaveChangesAsync().ConfigureAwait(false); + } + + public async Task AddChargingDetailsForAllCars() + { + logger.LogTrace("{method}()", nameof(AddChargingDetailsForAllCars)); + var availableSolarPower = GetSolarPower(); + foreach (var car in settings.CarsToManage.OrderBy(c => c.ChargingPriority)) + { + var chargingPowerAtHome = car.ChargingPowerAtHome ?? 0; + if (chargingPowerAtHome < 1) + { + logger.LogTrace("Car with ID {carId} 0 Watt chargingPower at home", car.Id); + continue; + } + var chargingDetail = await GetAttachedChargingDetail(car.Id); + if (chargingPowerAtHome < availableSolarPower) + { + chargingDetail.SolarPower = chargingPowerAtHome; + chargingDetail.GridPower = 0; + } + else + { + chargingDetail.SolarPower = availableSolarPower; + chargingDetail.GridPower = chargingPowerAtHome - availableSolarPower; + } + availableSolarPower -= chargingDetail.SolarPower; + if (availableSolarPower < 0) + { + availableSolarPower = 0; + } + } + await context.SaveChangesAsync().ConfigureAwait(false); + } + + private int GetSolarPower() + { + var solarPower = settings.Overage; + if (solarPower == default && settings.InverterPower != default) + { + //no grid meter available, so we have to calculate the power by using the inverter power and the power buffer + var powerBuffer = configurationWrapper.PowerBuffer(true); + solarPower = settings.InverterPower + //if powerBuffer is negative, it will be subtracted as it should be expected home power usage when no grid meter is available + - (powerBuffer > 0 ? powerBuffer : 0); + } + + if (solarPower == default) + { + logger.LogInformation("No solar power available, using 0 as default."); + return 0; + } + return (int)solarPower; + } + + private async Task GetAttachedChargingDetail(int carId) + { + var latestOpenChargingProcessId = await context.ChargingProcesses + .Where(cp => cp.CarId == carId && cp.EndDate == null) + .OrderByDescending(cp => cp.StartDate) + .Select(cp => cp.Id) + .FirstOrDefaultAsync().ConfigureAwait(false); + var chargingDetail = new ChargingDetail + { + TimeStamp = dateTimeProvider.UtcNow(), + }; + if (latestOpenChargingProcessId == default) + { + var chargingProcess = new ChargingProcess + { + StartDate = chargingDetail.TimeStamp, + CarId = carId, + }; + context.ChargingProcesses.Add(chargingProcess); + chargingProcess.ChargingDetails.Add(chargingDetail); + } + else + { + chargingDetail.ChargingProcessId = latestOpenChargingProcessId; + } + return chargingDetail; + } +} diff --git a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj index 502ad3ac1..9fac81b8b 100644 --- a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj +++ b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj @@ -76,6 +76,7 @@ + diff --git a/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs b/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs index c675f4a38..4fa1d6c43 100644 --- a/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs +++ b/TeslaSolarCharger/Shared/Dtos/Settings/DtoCar.cs @@ -4,6 +4,7 @@ namespace TeslaSolarCharger.Shared.Dtos.Settings; public class DtoCar { + private int? _chargingPower; public int Id { get; set; } public string Vin { get; set; } public int? TeslaMateCarId { get; set; } @@ -67,7 +68,19 @@ public int? ChargingPowerAtHome } } - private int? ChargingPower { get; set; } + private int? ChargingPower + { + get + { + if (_chargingPower == default) + { + return ChargerActualCurrent * ChargerVoltage * ActualPhases; + } + return _chargingPower; + } + set => _chargingPower = value; + } + public CarStateEnum? State { get; set; } public bool? Healthy { get; set; } public bool ReducedChargeSpeedWarning { get; set; }