@@ -18,7 +18,8 @@ type SystemConfig struct {
1818 MaxGridExport float64 // kW
1919 BatteryPreHeatPower float64 // kW - power consumption of battery preheating when active
2020 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)
21+ BatteryThermalTimeConstant float64 // fraction per hour - rate at which battery temperature approaches air temperature (0-1). This is automatically scaled based on TimeSlotDuration.
22+ TimeSlotDuration float64 // hours - duration of each time slot (e.g., 0.25 for 15 minutes, 1.0 for 1 hour). MUST match CheckPriceInterval configuration.
2223}
2324
2425// TimeSlot represents one time period of operation (typically 15 minutes, configurable via check_price_interval)
@@ -60,10 +61,10 @@ type ControlDecision struct {
6061
6162// Controller implements Model Predictive Control
6263type Controller struct {
63- Config SystemConfig
64- Horizon int // number of time periods to look ahead
65- CurrentSOC float64
66- CurrentBatteryTemp float64 // °C current battery temperature
64+ Config SystemConfig
65+ Horizon int // number of time periods to look ahead
66+ CurrentSOC float64
67+ CurrentBatteryTemp float64 // °C current battery temperature
6768}
6869
6970// NewController creates a new MPC controller
@@ -220,12 +221,31 @@ func (mpc *Controller) calculateNextBatteryTemp(currentTemp, airTemp float64, is
220221 // When charging with preheat, battery maintains temperature at threshold
221222 return math .Max (currentTemp , mpc .Config .BatteryPreHeatTempThreshold )
222223 }
223-
224+
225+ // Get time slot duration (default to 1 hour if not specified for backward compatibility)
226+ timeSlotDuration := mpc .Config .TimeSlotDuration
227+ if timeSlotDuration == 0 {
228+ timeSlotDuration = 1.0
229+ }
230+
231+ // Scale thermal time constant by time slot duration
232+ // BatteryThermalTimeConstant is defined per hour, so we scale it for the actual slot duration
233+ // For small k: k_slot ≈ k_hour * slot_duration_hours
234+ // For larger k, use exponential formula: k_slot = 1 - (1 - k_hour)^slot_duration_hours
235+ thermalConstant := mpc .Config .BatteryThermalTimeConstant
236+ if thermalConstant < 0.2 {
237+ // Use linear approximation for small values (more efficient)
238+ thermalConstant = thermalConstant * timeSlotDuration
239+ } else {
240+ // Use exponential formula for larger values (more accurate)
241+ thermalConstant = 1.0 - math .Pow (1.0 - thermalConstant , timeSlotDuration )
242+ }
243+
224244 // When not charging or warm enough, battery temperature moves toward air temperature
225245 // T(t+1) = T(t) + k * (T_air - T(t))
226246 // This models natural cooling/heating toward ambient air temperature
227247 tempDiff := airTemp - currentTemp
228- return currentTemp + mpc . Config . BatteryThermalTimeConstant * tempDiff
248+ return currentTemp + thermalConstant * tempDiff
229249}
230250
231251// generateFeasibleDecisions creates a set of feasible control decisions
@@ -281,7 +301,7 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, currentBatt
281301 for _ , action := range batteryActions {
282302 // Battery preheating is only active when we're actually charging and temp is below threshold
283303 preHeatActive := needsPreHeat && action .charge > 0
284-
304+
285305 dec := ControlDecision {
286306 Hour : slot .Hour ,
287307 Timestamp : slot .Timestamp ,
@@ -294,12 +314,12 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, currentBatt
294314 // When battery preheating is active (battery is charging at low temp), it consumes extra power from the grid
295315 netSolar := slot .SolarForecast
296316 extraLoad := 0.0
297-
317+
298318 // Battery preheating only consumes power when battery is charging
299319 if preHeatActive {
300320 extraLoad = preHeatPower
301321 }
302-
322+
303323 netLoad := slot .LoadForecast + action .charge / mpc .Config .BatteryEfficiency + extraLoad
304324 netSupply := netSolar + action .discharge * mpc .Config .BatteryEfficiency
305325
@@ -329,15 +349,25 @@ func (mpc *Controller) generateFeasibleDecisions(currentSOC float64, currentBatt
329349// Therefore, GridImport and GridExport already reflect the effect of battery operations and battery preheating.
330350// Profit is simply: revenue from exports - cost of imports - degradation cost
331351// Note: The battery preheating cost is already included in GridImport when battery is charging at low temperatures
352+ // All power values (kW) are multiplied by time slot duration (hours) to get energy (kWh)
332353func (mpc * Controller ) calculateProfit (dec ControlDecision , slot TimeSlot ) float64 {
354+ // Get time slot duration (default to 1 hour if not specified for backward compatibility)
355+ timeSlotDuration := mpc .Config .TimeSlotDuration
356+ if timeSlotDuration == 0 {
357+ timeSlotDuration = 1.0
358+ }
359+
333360 // Revenue from exporting to grid
334- revenue := dec .GridExport * slot .ExportPrice
361+ // GridExport is in kW, multiply by time slot duration to get kWh
362+ revenue := dec .GridExport * slot .ExportPrice * timeSlotDuration
335363
336364 // Cost of importing from grid (already includes battery preheating consumption when active)
337- importCost := dec .GridImport * slot .ImportPrice
365+ // GridImport is in kW, multiply by time slot duration to get kWh
366+ importCost := dec .GridImport * slot .ImportPrice * timeSlotDuration
338367
339368 // Battery degradation cost (wear and tear from cycling)
340- batteryThroughput := dec .BatteryCharge + dec .BatteryDischarge
369+ // Throughput is in kW, multiply by time slot duration to get kWh cycled
370+ batteryThroughput := (dec .BatteryCharge + dec .BatteryDischarge ) * timeSlotDuration
341371 degradationCost := batteryThroughput * mpc .Config .BatteryDegradationCost
342372
343373 // Net profit:
@@ -358,18 +388,42 @@ func (mpc *Controller) calculateProfit(dec ControlDecision, slot TimeSlot) float
358388
359389// Helper functions
360390func (mpc * Controller ) canCharge (soc , charge float64 ) bool {
361- newSOC := soc + (charge / mpc .Config .BatteryCapacity )
391+ // Get time slot duration (default to 1 hour if not specified for backward compatibility)
392+ timeSlotDuration := mpc .Config .TimeSlotDuration
393+ if timeSlotDuration == 0 {
394+ timeSlotDuration = 1.0
395+ }
396+
397+ // Convert power (kW) to energy (kWh) by multiplying by time slot duration
398+ chargeEnergy := charge * timeSlotDuration
399+ newSOC := soc + (chargeEnergy / mpc .Config .BatteryCapacity )
362400 return newSOC <= mpc .Config .BatteryMaxSOC
363401}
364402
365403func (mpc * Controller ) canDischarge (soc , discharge float64 ) bool {
366- newSOC := soc - (discharge / mpc .Config .BatteryCapacity )
404+ // Get time slot duration (default to 1 hour if not specified for backward compatibility)
405+ timeSlotDuration := mpc .Config .TimeSlotDuration
406+ if timeSlotDuration == 0 {
407+ timeSlotDuration = 1.0
408+ }
409+
410+ // Convert power (kW) to energy (kWh) by multiplying by time slot duration
411+ dischargeEnergy := discharge * timeSlotDuration
412+ newSOC := soc - (dischargeEnergy / mpc .Config .BatteryCapacity )
367413 return newSOC >= mpc .Config .BatteryMinSOC
368414}
369415
370416func (mpc * Controller ) calculateNewSOC (currentSOC , charge , discharge float64 ) float64 {
371- chargeEnergy := charge * mpc .Config .BatteryEfficiency
372- socChange := (chargeEnergy - discharge ) / mpc .Config .BatteryCapacity
417+ // Get time slot duration (default to 1 hour if not specified for backward compatibility)
418+ timeSlotDuration := mpc .Config .TimeSlotDuration
419+ if timeSlotDuration == 0 {
420+ timeSlotDuration = 1.0
421+ }
422+
423+ // Convert power (kW) to energy (kWh) by multiplying by time slot duration
424+ chargeEnergy := charge * timeSlotDuration * mpc .Config .BatteryEfficiency
425+ dischargeEnergy := discharge * timeSlotDuration
426+ socChange := (chargeEnergy - dischargeEnergy ) / mpc .Config .BatteryCapacity
373427 newSOC := currentSOC + socChange
374428 return math .Max (mpc .Config .BatteryMinSOC , math .Min (mpc .Config .BatteryMaxSOC , newSOC ))
375429}
0 commit comments