-
Notifications
You must be signed in to change notification settings - Fork 9
/
PriceHelper.cs
166 lines (153 loc) · 7.56 KB
/
PriceHelper.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TeslaMateAgile.Data.Options;
using TeslaMateAgile.Data.TeslaMate;
using TeslaMateAgile.Data.TeslaMate.Entities;
using TeslaMateAgile.Helpers.Interfaces;
using TeslaMateAgile.Services.Interfaces;
namespace TeslaMateAgile;
public class PriceHelper : IPriceHelper
{
private readonly ILogger<PriceHelper> _logger;
private readonly TeslaMateDbContext _context;
private readonly IPriceDataService _priceDataService;
private readonly TeslaMateOptions _teslaMateOptions;
public PriceHelper(
ILogger<PriceHelper> logger,
TeslaMateDbContext context,
IPriceDataService priceDataService,
IOptions<TeslaMateOptions> teslaMateOptions
)
{
_logger = logger;
_context = context;
_priceDataService = priceDataService;
_teslaMateOptions = teslaMateOptions.Value;
}
public async Task Update()
{
var geofence = await _context.Geofences.FirstOrDefaultAsync(x => x.Id == _teslaMateOptions.GeofenceId);
if (geofence == null)
{
_logger.LogWarning($"Configured geofence id does not exist in the TeslaMate database, make sure you have entered the correct id");
return;
}
else if (geofence.CostPerUnit.HasValue)
{
_logger.LogWarning($"Configured geofence '{geofence.Name}' (id: {geofence.Id}) should not have a cost set in TeslaMate as this may override TeslaMateAgile calculation");
return;
}
_logger.LogInformation($"Looking for finished charging processes with no cost set in the '{geofence.Name}' geofence (id: {geofence.Id})");
var chargingProcesses = await _context.ChargingProcesses
.Where(x => x.GeofenceId == _teslaMateOptions.GeofenceId && x.EndDate.HasValue && !x.Cost.HasValue)
.Include(x => x.Charges)
.ToListAsync();
if (!chargingProcesses.Any())
{
_logger.LogInformation("No new charging processes");
return;
}
foreach (var chargingProcess in chargingProcesses)
{
try
{
if (chargingProcess.Charges == null) { _logger.LogError($"Could not find charges on charging process {chargingProcess.Id}"); continue; }
var (cost, energy) = await CalculateChargeCost(chargingProcess.Charges);
_logger.LogInformation($"Calculated cost {cost} and energy {energy} kWh for charging process {chargingProcess.Id}");
if (chargingProcess.ChargeEnergyUsed.HasValue && chargingProcess.ChargeEnergyUsed.Value != energy)
{
_logger.LogWarning($"Mismatch between TeslaMate calculated energy used of {chargingProcess.ChargeEnergyUsed.Value} and ours of {energy}");
}
chargingProcess.Cost = cost;
}
catch (Exception e)
{
_logger.LogError(e, $"Failed to calculate charging cost / energy for charging process {chargingProcess.Id}");
}
}
await _context.SaveChangesAsync();
}
public async Task<(decimal Price, decimal Energy)> CalculateChargeCost(IEnumerable<Charge> charges)
{
var minDate = charges.Min(x => x.Date);
var maxDate = charges.Max(x => x.Date);
_logger.LogInformation($"Calculating cost for charges {minDate.UtcDateTime} UTC - {maxDate.UtcDateTime} UTC");
var prices = (await _priceDataService.GetPriceData(minDate, maxDate)).OrderBy(x => x.ValidFrom);
var totalPrice = 0M;
var totalEnergy = 0M;
Charge lastCharge = null;
var chargesCalculated = 0;
var phases = DeterminePhases(charges);
if (!phases.HasValue)
{
_logger.LogWarning($"Unable to determine phases for charges");
return (0, 0);
}
foreach (var price in prices)
{
var chargesForPrice = charges.Where(x => x.Date >= price.ValidFrom && x.Date < price.ValidTo).ToList();
chargesCalculated += chargesForPrice.Count;
if (lastCharge != null)
{
chargesForPrice.Add(lastCharge);
}
chargesForPrice = chargesForPrice.OrderBy(x => x.Date).ToList();
var energyAddedInDateRange = CalculateEnergyUsed(chargesForPrice, phases.Value);
var priceForEnergy = (energyAddedInDateRange * price.Value) + (energyAddedInDateRange * _teslaMateOptions.FeePerKilowattHour);
totalPrice += priceForEnergy;
totalEnergy += energyAddedInDateRange;
lastCharge = chargesForPrice.Last();
_logger.LogDebug($"Calculated charge cost for {price.ValidFrom.UtcDateTime} UTC - {price.ValidTo.UtcDateTime} UTC (unit cost: {price.Value}, fee per kWh: {_teslaMateOptions.FeePerKilowattHour}): {priceForEnergy} for {energyAddedInDateRange} energy");
}
var chargesCount = charges.Count();
if (chargesCalculated != chargesCount)
{
throw new Exception($"Charge calculation failed, pricing calculated for {chargesCalculated} / {chargesCount}, likely missing price data");
}
return (Math.Round(totalPrice, 2), Math.Round(totalEnergy, 2));
}
public decimal CalculateEnergyUsed(IEnumerable<Charge> charges, decimal phases)
{
// adapted from https://github.com/adriankumpf/teslamate/blob/0db6d6905ce804b3b8cafc0ab69aa8cd346446a8/lib/teslamate/log.ex#L464-L488
var power = charges
.Select(c => !c.ChargerPhases.HasValue ?
c.ChargerPower :
((c.ChargerActualCurrent ?? 0) * (c.ChargerVoltage ?? 0) * phases / 1000M)
* (charges.Any(x => x.Date < c.Date) ?
(decimal)(c.Date - charges.OrderByDescending(x => x.Date).First(x => x.Date < c.Date).Date).TotalHours
: (decimal?)null)
);
return power
.Where(x => x.HasValue && x.Value >= 0)
.Sum(x => x.Value);
}
public decimal? DeterminePhases(IEnumerable<Charge> charges)
{
// adapted from https://github.com/adriankumpf/teslamate/blob/0db6d6905ce804b3b8cafc0ab69aa8cd346446a8/lib/teslamate/log.ex#L490-L527
var powerAverage = charges.Where(x => x.ChargerActualCurrent.HasValue && x.ChargerVoltage.HasValue)
.Select(x => x.ChargerPower * 1000.0 / (x.ChargerActualCurrent.Value * x.ChargerVoltage.Value))
.Where(x => !double.IsNaN(x))
.Average();
var phasesAverage = (int)charges.Where(x => x.ChargerPhases.HasValue).Average(x => x.ChargerPhases.Value);
var voltageAverage = charges.Where(x => x.ChargerVoltage.HasValue).Average(x => x.ChargerVoltage.Value);
if (powerAverage > 0 && charges.Count() > 15)
{
if (phasesAverage == Math.Round(powerAverage))
{
return phasesAverage;
}
if (phasesAverage == 3 && Math.Abs(powerAverage / Math.Sqrt(phasesAverage) - 1) <= 0.1)
{
_logger.LogInformation($"Voltage correction: {Math.Round(voltageAverage)}V -> {Math.Round(voltageAverage / Math.Sqrt(phasesAverage))}V");
return (decimal)Math.Sqrt(phasesAverage);
}
if (Math.Abs(Math.Round(powerAverage) - powerAverage) <= 0.3)
{
_logger.LogInformation($"Phase correction: {phasesAverage} -> {Math.Round(powerAverage)}");
return (decimal)Math.Round(powerAverage);
}
}
return null;
}
}