Skip to content

Commit 162ded3

Browse files
committed
feat: show temp data in the mpc table
1 parent 284917f commit 162ded3

6 files changed

Lines changed: 326 additions & 106 deletions

File tree

‎mpc/mpc.go‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type TimeSlot struct {
2828
LoadForecast float64 // kW average for the time period
2929
CloudCoverage float64 // % cloud coverage (0-100)
3030
WeatherSymbol string // weather condition symbol
31+
AirTemperature float64 // °C air temperature
3132
}
3233

3334
// ControlDecision represents the optimal control for one time slot (typically 15 minutes, configurable via check_price_interval)
@@ -43,12 +44,14 @@ type ControlDecision struct {
4344
BatterySOC float64 // percentage (0-1)
4445
Profit float64 // $ for this time period
4546
// Forecast data used for this decision
46-
ImportPrice float64 // $/kWh
47-
ExportPrice float64 // $/kWh
48-
SolarForecast float64 // kW average for the time period
49-
LoadForecast float64 // kW average for the time period
50-
CloudCoverage float64 // % cloud coverage (0-100)
51-
WeatherSymbol string // weather condition symbol
47+
ImportPrice float64 // $/kWh
48+
ExportPrice float64 // $/kWh
49+
SolarForecast float64 // kW average for the time period
50+
LoadForecast float64 // kW average for the time period
51+
CloudCoverage float64 // % cloud coverage (0-100)
52+
WeatherSymbol string // weather condition symbol
53+
BatteryAvgCellTemp float64 // °C average cell temperature
54+
AirTemperature float64 // °C air temperature
5255
}
5356

5457
// Controller implements Model Predictive Control
@@ -167,6 +170,7 @@ func (mpc *Controller) optimizeWithForecast(forecast []TimeSlot, includeSolar bo
167170
dp[t+1][newSOCIdx].decision.LoadForecast = slot.LoadForecast
168171
dp[t+1][newSOCIdx].decision.CloudCoverage = slot.CloudCoverage
169172
dp[t+1][newSOCIdx].decision.WeatherSymbol = slot.WeatherSymbol
173+
dp[t+1][newSOCIdx].decision.AirTemperature = slot.AirTemperature
170174
dp[t+1][newSOCIdx].prevSOC = socIdx
171175
}
172176
}

‎scheduler/mpc.go‎

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) error {
7272
return nil
7373
}
7474

75+
// Step 4.1: Add battery temperature to all decisions
76+
// Use current temperature as baseline (future temperature forecasting could be added later)
77+
batteryTemp := plantInfo.ESSAvgCellTemperature
78+
for i := range decisions {
79+
decisions[i].BatteryAvgCellTemp = batteryTemp
80+
}
81+
7582
// Step 5: Save optimization results to memory
7683
s.mu.Lock()
7784
s.mpcDecisions = decisions
@@ -216,14 +223,15 @@ func (s *MinerScheduler) buildMPCForecast(ctx context.Context, config *Config, p
216223
loadForecast := s.estimateLoadForecast(importPrice*1000.0, config.PriceLimit/1000, solar, config)
217224

218225
timeSlots = append(timeSlots, mpc.TimeSlot{
219-
Hour: i, // Now represents time slot index, not hour
220-
Timestamp: futureTime.Unix(),
221-
ImportPrice: importPrice,
222-
ExportPrice: exportPrice,
223-
SolarForecast: solar,
224-
LoadForecast: loadForecast,
225-
CloudCoverage: weather.CloudCoverage,
226-
WeatherSymbol: weather.WeatherSymbol,
226+
Hour: i, // Now represents time slot index, not hour
227+
Timestamp: futureTime.Unix(),
228+
ImportPrice: importPrice,
229+
ExportPrice: exportPrice,
230+
SolarForecast: solar,
231+
LoadForecast: loadForecast,
232+
CloudCoverage: weather.CloudCoverage,
233+
WeatherSymbol: weather.WeatherSymbol,
234+
AirTemperature: weather.AirTemperature,
227235
})
228236
}
229237

@@ -232,8 +240,9 @@ func (s *MinerScheduler) buildMPCForecast(ctx context.Context, config *Config, p
232240

233241
// WeatherData represents weather information for a specific hour
234242
type WeatherData struct {
235-
CloudCoverage float64 // % cloud coverage (0-100)
236-
WeatherSymbol string // weather condition symbol
243+
CloudCoverage float64 // % cloud coverage (0-100)
244+
WeatherSymbol string // weather condition symbol
245+
AirTemperature float64 // °C air temperature
237246
}
238247

239248
// getSolarForecast gets solar power forecast from weather data
@@ -254,11 +263,12 @@ func (s *MinerScheduler) getSolarForecast(config *Config, now time.Time, weather
254263

255264
for i := range 36 {
256265
futureTime := now.Add(time.Duration(i) * time.Hour)
257-
solarPower, cloudCoverage, weatherSymbol := s.estimateSolarPowerFromWeather(weatherForecast, futureTime, config.MaxSolarPower, currentPVPower)
266+
solarPower, cloudCoverage, weatherSymbol, airTemp := s.estimateSolarPowerFromWeather(weatherForecast, futureTime, config.MaxSolarPower, currentPVPower)
258267
solarForecast[i] = solarPower
259268
weatherData[i] = WeatherData{
260-
CloudCoverage: cloudCoverage,
261-
WeatherSymbol: weatherSymbol,
269+
CloudCoverage: cloudCoverage,
270+
WeatherSymbol: weatherSymbol,
271+
AirTemperature: airTemp,
262272
}
263273
}
264274
solarForecast[0] = currentPVPower
@@ -293,12 +303,13 @@ func (s *MinerScheduler) getOrFetchWeatherForecast(config *Config) (*meteo.METJS
293303
}
294304

295305
// estimateSolarPowerFromWeather estimates solar power output from weather data
296-
func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONForecast, targetTime time.Time, peakPower float64, currentPVPower float64) (float64, float64, string) {
306+
func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONForecast, targetTime time.Time, peakPower float64, currentPVPower float64) (float64, float64, string, float64) {
297307
cloudCoverage := 0.0
298308
weatherSymbol := ""
309+
airTemperature := 0.0
299310

300311
if forecast.Properties == nil || len(forecast.Properties.Timeseries) == 0 {
301-
return 0, cloudCoverage, weatherSymbol
312+
return 0, cloudCoverage, weatherSymbol, airTemperature
302313
}
303314

304315
// Find closest time step
@@ -317,7 +328,7 @@ func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONFo
317328
}
318329

319330
if closestStep == nil || closestStep.Data == nil || closestStep.Data.Instant == nil || closestStep.Data.Instant.Details == nil {
320-
return 0, cloudCoverage, weatherSymbol
331+
return 0, cloudCoverage, weatherSymbol, airTemperature
321332
}
322333

323334
details := closestStep.Data.Instant.Details
@@ -332,6 +343,11 @@ func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONFo
332343
weatherSymbol = string(*symbol)
333344
}
334345

346+
// Get air temperature
347+
if details.AirTemperature != nil {
348+
airTemperature = *details.AirTemperature
349+
}
350+
335351
// Get location from config
336352
config := s.GetConfig()
337353
lat := config.Latitude
@@ -344,7 +360,7 @@ func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONFo
344360

345361
// Check if we're between sunrise and sunset
346362
if targetTime.Before(sunrise) || targetTime.After(sunset) {
347-
return 0, cloudCoverage, weatherSymbol // No sun available
363+
return 0, cloudCoverage, weatherSymbol, airTemperature // No sun available
348364
}
349365

350366
// Get solar position to calculate altitude angle
@@ -356,14 +372,14 @@ func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONFo
356372
// Use sine of altitude as a factor (0 at horizon, 1 at zenith)
357373
solarAngleFactor := math.Sin(altitude)
358374
if solarAngleFactor < 0 {
359-
return 0, cloudCoverage, weatherSymbol
375+
return 0, cloudCoverage, weatherSymbol, airTemperature
360376
}
361377

362378
// Check for snow conditions - PV panels covered by snow produce zero power
363379
if symbol := closestStep.GetSymbolCode(); symbol != nil {
364380
if symbol.HasSnow() {
365381
s.logger.Printf("Snow detected in weather forecast at %s, setting solar power to zero", targetTime.Format(time.RFC3339))
366-
return 0, cloudCoverage, weatherSymbol
382+
return 0, cloudCoverage, weatherSymbol, airTemperature
367383
}
368384
}
369385

@@ -373,7 +389,7 @@ func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONFo
373389
if currentPVPower < 0.1 && expectedPower > 1.0 && time.Until(targetTime).Hours() < 1 {
374390
// Current power is essentially zero but we expect power - likely snow covered
375391
s.logger.Printf("Current PV power is zero (%.2f kW) but forecast expects %.2f kW - panels may be snow covered", currentPVPower, expectedPower)
376-
return 0, cloudCoverage, weatherSymbol
392+
return 0, cloudCoverage, weatherSymbol, airTemperature
377393
}
378394

379395
// Cloud factor (0-1, where 1 = no clouds)
@@ -386,7 +402,7 @@ func (s *MinerScheduler) estimateSolarPowerFromWeather(forecast *meteo.METJSONFo
386402
// Estimate solar power
387403
solarPower := peakPower * solarAngleFactor * cloudFactor
388404

389-
return solarPower, cloudCoverage, weatherSymbol
405+
return solarPower, cloudCoverage, weatherSymbol, airTemperature
390406
}
391407

392408
// estimateLoadForecast estimates power load based on price and available power

‎scheduler/server.go‎

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,14 @@ type MPCDecisionInfo struct {
6060
BatterySOC float64 `json:"battery_soc"`
6161
Profit float64 `json:"profit"`
6262
// Forecast data used for this decision
63-
ImportPrice float64 `json:"import_price"`
64-
ExportPrice float64 `json:"export_price"`
65-
SolarForecast float64 `json:"solar_forecast"`
66-
LoadForecast float64 `json:"load_forecast"`
67-
CloudCoverage float64 `json:"cloud_coverage"`
68-
WeatherSymbol string `json:"weather_symbol"`
63+
ImportPrice float64 `json:"import_price"`
64+
ExportPrice float64 `json:"export_price"`
65+
SolarForecast float64 `json:"solar_forecast"`
66+
LoadForecast float64 `json:"load_forecast"`
67+
CloudCoverage float64 `json:"cloud_coverage"`
68+
WeatherSymbol string `json:"weather_symbol"`
69+
BatteryAvgCellTemp float64 `json:"battery_avg_cell_temp"`
70+
AirTemperature float64 `json:"air_temperature"`
6971
}
7072

7173
// SystemHealth represents system-level health information
@@ -202,20 +204,22 @@ func (hs *WebServer) healthHandler(w http.ResponseWriter, r *http.Request) {
202204
mpcDecisionsInfo := make([]MPCDecisionInfo, 0, len(mpcDecisions))
203205
for _, dec := range mpcDecisions {
204206
mpcDecisionsInfo = append(mpcDecisionsInfo, MPCDecisionInfo{
205-
Hour: dec.Hour,
206-
Timestamp: dec.Timestamp,
207-
BatteryCharge: dec.BatteryCharge,
208-
BatteryDischarge: dec.BatteryDischarge,
209-
GridImport: dec.GridImport,
210-
GridExport: dec.GridExport,
211-
BatterySOC: dec.BatterySOC,
212-
Profit: dec.Profit,
213-
ImportPrice: dec.ImportPrice,
214-
ExportPrice: dec.ExportPrice,
215-
SolarForecast: dec.SolarForecast,
216-
LoadForecast: dec.LoadForecast,
217-
CloudCoverage: dec.CloudCoverage,
218-
WeatherSymbol: dec.WeatherSymbol,
207+
Hour: dec.Hour,
208+
Timestamp: dec.Timestamp,
209+
BatteryCharge: dec.BatteryCharge,
210+
BatteryDischarge: dec.BatteryDischarge,
211+
GridImport: dec.GridImport,
212+
GridExport: dec.GridExport,
213+
BatterySOC: dec.BatterySOC,
214+
Profit: dec.Profit,
215+
ImportPrice: dec.ImportPrice,
216+
ExportPrice: dec.ExportPrice,
217+
SolarForecast: dec.SolarForecast,
218+
LoadForecast: dec.LoadForecast,
219+
CloudCoverage: dec.CloudCoverage,
220+
WeatherSymbol: dec.WeatherSymbol,
221+
BatteryAvgCellTemp: dec.BatteryAvgCellTemp,
222+
AirTemperature: dec.AirTemperature,
219223
})
220224
}
221225

@@ -498,20 +502,22 @@ func (hs *WebServer) buildStatusData() map[string]any {
498502
mpcDecisionsInfo := make([]MPCDecisionInfo, 0, len(mpcDecisions))
499503
for _, dec := range mpcDecisions {
500504
mpcDecisionsInfo = append(mpcDecisionsInfo, MPCDecisionInfo{
501-
Hour: dec.Hour,
502-
Timestamp: dec.Timestamp,
503-
BatteryCharge: dec.BatteryCharge,
504-
BatteryDischarge: dec.BatteryDischarge,
505-
GridImport: dec.GridImport,
506-
GridExport: dec.GridExport,
507-
BatterySOC: dec.BatterySOC,
508-
Profit: dec.Profit,
509-
ImportPrice: dec.ImportPrice,
510-
ExportPrice: dec.ExportPrice,
511-
SolarForecast: dec.SolarForecast,
512-
LoadForecast: dec.LoadForecast,
513-
CloudCoverage: dec.CloudCoverage,
514-
WeatherSymbol: dec.WeatherSymbol,
505+
Hour: dec.Hour,
506+
Timestamp: dec.Timestamp,
507+
BatteryCharge: dec.BatteryCharge,
508+
BatteryDischarge: dec.BatteryDischarge,
509+
GridImport: dec.GridImport,
510+
GridExport: dec.GridExport,
511+
BatterySOC: dec.BatterySOC,
512+
Profit: dec.Profit,
513+
ImportPrice: dec.ImportPrice,
514+
ExportPrice: dec.ExportPrice,
515+
SolarForecast: dec.SolarForecast,
516+
LoadForecast: dec.LoadForecast,
517+
CloudCoverage: dec.CloudCoverage,
518+
WeatherSymbol: dec.WeatherSymbol,
519+
BatteryAvgCellTemp: dec.BatteryAvgCellTemp,
520+
AirTemperature: dec.AirTemperature,
515521
})
516522
}
517523

0 commit comments

Comments
 (0)