Skip to content

Commit 9807376

Browse files
committed
feat: check avg cell temp to predict preheat load
1 parent 162ded3 commit 9807376

9 files changed

Lines changed: 751 additions & 100 deletions

File tree

‎mpc/mpc.go‎

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,32 @@ import (
77

88
// SystemConfig holds the inverter system configuration
99
type SystemConfig struct {
10-
BatteryCapacity float64 // kWh
11-
BatteryMaxCharge float64 // kW
12-
BatteryMaxDischarge float64 // kW
13-
BatteryMinSOC float64 // percentage (0-1)
14-
BatteryMaxSOC float64 // percentage (0-1)
15-
BatteryEfficiency float64 // round-trip efficiency (0-1)
16-
BatteryDegradationCost float64 // $/kWh cycled
17-
MaxGridImport float64 // kW
18-
MaxGridExport float64 // kW
10+
BatteryCapacity float64 // kWh
11+
BatteryMaxCharge float64 // kW
12+
BatteryMaxDischarge float64 // kW
13+
BatteryMinSOC float64 // percentage (0-1)
14+
BatteryMaxSOC float64 // percentage (0-1)
15+
BatteryEfficiency float64 // round-trip efficiency (0-1)
16+
BatteryDegradationCost float64 // $/kWh cycled
17+
MaxGridImport float64 // kW
18+
MaxGridExport float64 // kW
19+
BatteryPreHeatPower float64 // kW - power consumption of battery preheating when active
20+
BatteryPreHeatTempThreshold float64 // °C - temperature threshold below which battery preheating activates
21+
BatteryThermalTimeConstant float64 // fraction per time slot - rate at which battery temperature approaches air temperature (0-1)
1922
}
2023

2124
// TimeSlot represents one time period of operation (typically 15 minutes, configurable via check_price_interval)
2225
type TimeSlot struct {
23-
Hour int
24-
Timestamp int64 // Unix timestamp when this time slot begins
25-
ImportPrice float64 // $/kWh
26-
ExportPrice float64 // $/kWh
27-
SolarForecast float64 // kW average for the time period
28-
LoadForecast float64 // kW average for the time period
29-
CloudCoverage float64 // % cloud coverage (0-100)
30-
WeatherSymbol string // weather condition symbol
31-
AirTemperature float64 // °C air temperature
26+
Hour int
27+
Timestamp int64 // Unix timestamp when this time slot begins
28+
ImportPrice float64 // $/kWh
29+
ExportPrice float64 // $/kWh
30+
SolarForecast float64 // kW average for the time period
31+
LoadForecast float64 // kW average for the time period
32+
CloudCoverage float64 // % cloud coverage (0-100)
33+
WeatherSymbol string // weather condition symbol
34+
AirTemperature float64 // °C air temperature
35+
BatteryAvgCellTemp float64 // °C average cell temperature
3236
}
3337

3438
// ControlDecision represents the optimal control for one time slot (typically 15 minutes, configurable via check_price_interval)
@@ -43,6 +47,7 @@ type ControlDecision struct {
4347
GridExport float64 // kW (positive = exporting)
4448
BatterySOC float64 // percentage (0-1)
4549
Profit float64 // $ for this time period
50+
BatteryPreHeatActive bool // true if battery preheating is active during this time slot
4651
// Forecast data used for this decision
4752
ImportPrice float64 // $/kWh
4853
ExportPrice float64 // $/kWh
@@ -171,6 +176,7 @@ func (mpc *Controller) optimizeWithForecast(forecast []TimeSlot, includeSolar bo
171176
dp[t+1][newSOCIdx].decision.CloudCoverage = slot.CloudCoverage
172177
dp[t+1][newSOCIdx].decision.WeatherSymbol = slot.WeatherSymbol
173178
dp[t+1][newSOCIdx].decision.AirTemperature = slot.AirTemperature
179+
dp[t+1][newSOCIdx].decision.BatteryAvgCellTemp = slot.BatteryAvgCellTemp
174180
dp[t+1][newSOCIdx].prevSOC = socIdx
175181
}
176182
}
@@ -203,6 +209,14 @@ func (mpc *Controller) optimizeWithForecast(forecast []TimeSlot, includeSolar bo
203209
func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, slot TimeSlot) []ControlDecision {
204210
decisions := []ControlDecision{}
205211

212+
// Determine if battery preheating would be needed based on battery temperature
213+
// Battery preheating is only active when actually charging the battery
214+
needsPreHeat := mpc.Config.BatteryPreHeatPower > 0 && slot.BatteryAvgCellTemp < mpc.Config.BatteryPreHeatTempThreshold
215+
preHeatPower := 0.0
216+
if needsPreHeat {
217+
preHeatPower = mpc.Config.BatteryPreHeatPower
218+
}
219+
206220
// Always include idle option
207221
batteryActions := []struct {
208222
charge float64
@@ -242,16 +256,28 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, slot TimeSl
242256

243257
// For each battery action, calculate power balance
244258
for _, action := range batteryActions {
259+
// Battery preheating is only active when we're actually charging and temp is below threshold
260+
preHeatActive := needsPreHeat && action.charge > 0
261+
245262
dec := ControlDecision{
246-
Hour: slot.Hour,
247-
Timestamp: slot.Timestamp,
248-
BatteryCharge: action.charge,
249-
BatteryDischarge: action.discharge,
263+
Hour: slot.Hour,
264+
Timestamp: slot.Timestamp,
265+
BatteryCharge: action.charge,
266+
BatteryDischarge: action.discharge,
267+
BatteryPreHeatActive: preHeatActive,
250268
}
251269

252-
// Power balance: Solar + GridImport + BatteryDischarge = Load + GridExport + BatteryCharge
270+
// Power balance: Solar + GridImport + BatteryDischarge = Load + GridExport + BatteryCharge + BatteryPreHeat
271+
// When battery preheating is active (battery is charging at low temp), it consumes extra power from the grid
253272
netSolar := slot.SolarForecast
254-
netLoad := slot.LoadForecast + action.charge/mpc.Config.BatteryEfficiency
273+
extraLoad := 0.0
274+
275+
// Battery preheating only consumes power when battery is charging
276+
if preHeatActive {
277+
extraLoad = preHeatPower
278+
}
279+
280+
netLoad := slot.LoadForecast + action.charge/mpc.Config.BatteryEfficiency + extraLoad
255281
netSupply := netSolar + action.discharge*mpc.Config.BatteryEfficiency
256282

257283
balance := netSupply - netLoad
@@ -276,14 +302,15 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, slot TimeSl
276302
}
277303

278304
// calculateProfit computes the profit for a decision
279-
// The power balance equation ensures: Solar + GridImport + BatteryDischarge*eff = Load + GridExport + BatteryCharge/eff
280-
// Therefore, GridImport and GridExport already reflect the effect of battery operations.
305+
// The power balance equation ensures: Solar + GridImport + BatteryDischarge*eff = Load + GridExport + BatteryCharge/eff + BatteryPreHeat
306+
// Therefore, GridImport and GridExport already reflect the effect of battery operations and battery preheating.
281307
// Profit is simply: revenue from exports - cost of imports - degradation cost
308+
// Note: The battery preheating cost is already included in GridImport when battery is charging at low temperatures
282309
func (mpc *Controller) calculateProfit(dec ControlDecision, slot TimeSlot) float64 {
283310
// Revenue from exporting to grid
284311
revenue := dec.GridExport * slot.ExportPrice
285312

286-
// Cost of importing from grid
313+
// Cost of importing from grid (already includes battery preheating consumption when active)
287314
importCost := dec.GridImport * slot.ImportPrice
288315

289316
// Battery degradation cost (wear and tear from cycling)
@@ -292,13 +319,15 @@ func (mpc *Controller) calculateProfit(dec ControlDecision, slot TimeSlot) float
292319

293320
// Net profit:
294321
// + Revenue from exports (GridExport already accounts for battery discharge to grid)
295-
// - Cost of imports (GridImport already accounts for battery charging and reduced imports from discharge)
322+
// - Cost of imports (GridImport already accounts for battery charging, battery preheating, and reduced imports from discharge)
296323
// - Battery degradation (wear and tear cost)
297324
//
298325
// This correctly incentivizes arbitrage:
299326
// - Charging at low import prices reduces profit by importCost
300327
// - Discharging at high export prices increases profit by revenue
328+
// - When battery temp is low (<10°C), charging incurs additional battery preheating cost (700W)
301329
// - The DP optimizer will naturally prefer charge-low/discharge-high strategies
330+
// - The optimizer will avoid charging at low temperatures unless prices are very favorable
302331
profit := revenue - importCost - degradationCost
303332

304333
return profit

0 commit comments

Comments
 (0)