@@ -7,28 +7,32 @@ import (
77
88// SystemConfig holds the inverter system configuration
99type 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)
2225type 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
203209func (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
282309func (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