Skip to content

Commit 2a2a9a4

Browse files
authored
feat: add 1min default task retry interval (#12)
1 parent d06088f commit 2a2a9a4

7 files changed

Lines changed: 117 additions & 84 deletions

File tree

‎main.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,16 @@ func runMPCOptimize(config *scheduler.Config) {
108108

109109
ctx := context.Background()
110110

111-
minerScheduler.RunMinerDiscovery(ctx)
111+
if err := minerScheduler.RunMinerDiscovery(ctx); err != nil {
112+
logger.Printf("Error during miner discovery: %v", err)
113+
}
112114

113115
// Run MPC optimization
114116
logger.Printf("Running MPC optimization...")
115-
minerScheduler.RunMPCOptimize(ctx)
117+
if err := minerScheduler.RunMPCOptimize(ctx); err != nil {
118+
logger.Printf("Error during MPC optimization: %v", err)
119+
return
120+
}
116121

117122
// Get and log all decisions
118123
decisions := minerScheduler.GetMPCDecisions()

‎mpc/mpc_test.go‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ func TestOptimize(t *testing.T) {
452452
BatteryMinSOC: 0.0, // 0% minimum SOC
453453
BatteryMaxSOC: 1.0, // 100% maximum SOC
454454
BatteryEfficiency: 0.9, // 90% round-trip efficiency
455-
BatteryDegradationCost: 0.0, // €0.00/kWh degradation cost
455+
BatteryDegradationCost: 0.0, // 0.00/kWh degradation cost
456456
MaxGridImport: 30.0, // 30 kW max grid import
457457
MaxGridExport: 30.0, // 30 kW max grid export
458458
}
@@ -474,7 +474,7 @@ func TestOptimize(t *testing.T) {
474474

475475
// Verify all decisions have valid data
476476
for i, dec := range decisions {
477-
t.Logf("Hour %2d: SOC=%.3f, Charge=%.3f, Discharge=%.3f, Import=%.3f, Export=%.3f, Profit=€%.4f, Price=€%.5f/kWh",
477+
t.Logf("Hour %2d: SOC=%.3f, Charge=%.3f, Discharge=%.3f, Import=%.3f, Export=%.3f, Profit=%.4f, Price=%.5f/kWh",
478478
i, dec.BatterySOC, dec.BatteryCharge, dec.BatteryDischarge,
479479
dec.GridImport, dec.GridExport, dec.Profit, dec.ImportPrice)
480480

@@ -527,7 +527,7 @@ func TestOptimize(t *testing.T) {
527527
for _, dec := range decisions {
528528
totalProfit += dec.Profit
529529
}
530-
t.Logf("Total profit over 36 hours: €%.4f", totalProfit)
530+
t.Logf("Total profit over 36 hours: %.4f", totalProfit)
531531

532532
// Detailed arbitrage analysis
533533
t.Log("\n=== Arbitrage Analysis ===")
@@ -543,17 +543,17 @@ func TestOptimize(t *testing.T) {
543543
exportPriceHour22 := forecast[22].ExportPrice
544544

545545
t.Logf("\nHour 4 (LOWEST PRICE):")
546-
t.Logf(" Import Price: €%.5f/kWh", importPriceHour4)
547-
t.Logf(" Export Price: €%.5f/kWh", exportPriceHour4)
546+
t.Logf(" Import Price: %.5f/kWh", importPriceHour4)
547+
t.Logf(" Export Price: %.5f/kWh", exportPriceHour4)
548548
t.Logf(" Charged: %.3f kW (added %.3f kWh to battery)", hour4.BatteryCharge, hour4.BatteryCharge*0.9)
549-
t.Logf(" Cost: €%.4f (import + degradation)", hour4.BatteryCharge*importPriceHour4+hour4.BatteryCharge*config.BatteryDegradationCost)
549+
t.Logf(" Cost: %.4f (import + degradation)", hour4.BatteryCharge*importPriceHour4+hour4.BatteryCharge*config.BatteryDegradationCost)
550550

551551
t.Logf("\nHour 22 (HIGHEST PRICE):")
552-
t.Logf(" Import Price: €%.5f/kWh", importPriceHour22)
553-
t.Logf(" Export Price: €%.5f/kWh", exportPriceHour22)
552+
t.Logf(" Import Price: %.5f/kWh", importPriceHour22)
553+
t.Logf(" Export Price: %.5f/kWh", exportPriceHour22)
554554
t.Logf(" Discharged: %.3f kW (%.3f kWh from battery)", hour22.BatteryDischarge, hour22.BatteryDischarge)
555555
t.Logf(" Exported: %.3f kW", hour22.GridExport)
556-
t.Logf(" Revenue: €%.4f (export - degradation)", hour22.GridExport*exportPriceHour22-hour22.BatteryDischarge*config.BatteryDegradationCost)
556+
t.Logf(" Revenue: %.4f (export - degradation)", hour22.GridExport*exportPriceHour22-hour22.BatteryDischarge*config.BatteryDegradationCost)
557557

558558
// Calculate theoretical max arbitrage if charged more at hour 4
559559
maxAdditionalCharge := config.BatteryMaxCharge - hour4.BatteryCharge
@@ -564,9 +564,9 @@ func TestOptimize(t *testing.T) {
564564
t.Logf("\nTheoretical Additional Arbitrage Opportunity:")
565565
t.Logf(" Could charge additional: %.3f kW at hour 4", actualMaxCharge)
566566
t.Logf(" Energy stored after efficiency: %.3f kWh", actualMaxCharge*config.BatteryEfficiency)
567-
t.Logf(" Cost to charge: €%.4f", actualMaxCharge*importPriceHour4+actualMaxCharge*config.BatteryDegradationCost)
568-
t.Logf(" Revenue if exported at hour 22: €%.4f", actualMaxCharge*config.BatteryEfficiency*exportPriceHour22-actualMaxCharge*config.BatteryDegradationCost)
569-
t.Logf(" Net profit from additional arbitrage: €%.4f",
567+
t.Logf(" Cost to charge: %.4f", actualMaxCharge*importPriceHour4+actualMaxCharge*config.BatteryDegradationCost)
568+
t.Logf(" Revenue if exported at hour 22: %.4f", actualMaxCharge*config.BatteryEfficiency*exportPriceHour22-actualMaxCharge*config.BatteryDegradationCost)
569+
t.Logf(" Net profit from additional arbitrage: %.4f",
570570
actualMaxCharge*config.BatteryEfficiency*exportPriceHour22-
571571
actualMaxCharge*importPriceHour4-
572572
2*actualMaxCharge*config.BatteryDegradationCost)
@@ -579,7 +579,7 @@ func TestOptimize(t *testing.T) {
579579
for i, dec := range decisions {
580580
if dec.BatteryCharge > 0.1 {
581581
chargedTotal += dec.BatteryCharge
582-
t.Logf(" Hour %2d: CHARGED %.3f kW at price €%.5f/kWh (SOC: %.3f)",
582+
t.Logf(" Hour %2d: CHARGED %.3f kW at price %.5f/kWh (SOC: %.3f)",
583583
i, dec.BatteryCharge, dec.ImportPrice, dec.BatterySOC)
584584
}
585585
if dec.BatteryDischarge > 0.1 {
@@ -588,12 +588,12 @@ func TestOptimize(t *testing.T) {
588588
if dec.GridExport > 0.001 {
589589
exported = fmt.Sprintf(" → EXPORTED %.3f kW", dec.GridExport)
590590
}
591-
t.Logf(" Hour %2d: DISCHARGED %.3f kW at price €%.5f/kWh (SOC: %.3f)%s",
591+
t.Logf(" Hour %2d: DISCHARGED %.3f kW at price %.5f/kWh (SOC: %.3f)%s",
592592
i, dec.BatteryDischarge, dec.ExportPrice, dec.BatterySOC, exported)
593593
}
594594
}
595595
t.Logf("\nTotal charged: %.3f kW, Total discharged: %.3f kW", chargedTotal, dischargedTotal)
596-
t.Logf("Degradation cost: €%.4f (%.3f kW throughput × €%.2f/kWh)",
596+
t.Logf("Degradation cost: %.4f (%.3f kW throughput × %.2f/kWh)",
597597
(chargedTotal+dischargedTotal)*config.BatteryDegradationCost,
598598
chargedTotal+dischargedTotal,
599599
config.BatteryDegradationCost)
@@ -617,8 +617,8 @@ func TestOptimize(t *testing.T) {
617617
}
618618
}
619619

620-
t.Logf("Lowest price: €%.5f/kWh at hour %d", minPrice, minPriceHour)
621-
t.Logf("Highest price: €%.5f/kWh at hour %d", maxPrice, maxPriceHour)
620+
t.Logf("Lowest price: %.5f/kWh at hour %d", minPrice, minPriceHour)
621+
t.Logf("Highest price: %.5f/kWh at hour %d", maxPrice, maxPriceHour)
622622

623623
// Check that battery tends to charge during low prices
624624
// (Not strict since it depends on SOC constraints and overall optimization)

‎scheduler/data.go‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,20 @@ func (d *DataSamples) GetLatestPower() float64 {
142142
return d.samples[len(d.samples)-1].pvPower
143143
}
144144

145-
func (s *MinerScheduler) runDataPoll(samples *DataSamples) {
145+
func (s *MinerScheduler) runDataPoll(samples *DataSamples) error {
146146
if s.config.PlantModbusAddress == "" {
147-
return
147+
return nil
148148
}
149149
client, err := sigenergy.NewTCPClient(s.config.PlantModbusAddress, sigenergy.PlantAddress)
150150
if err != nil {
151151
s.logger.Printf("Data integration: failed to create modbus client: %v", err)
152-
return
152+
return err
153153
}
154154
defer client.Close()
155155
info, err := client.ReadPlantRunningInfo()
156156
if err != nil {
157157
s.logger.Printf("Data integration: failed to read PlantRunningInfo: %v", err)
158-
return
158+
return err
159159
}
160160
samples.AddSample(
161161
info.PhotovoltaicPower,
@@ -165,19 +165,20 @@ func (s *MinerScheduler) runDataPoll(samples *DataSamples) {
165165
info.ESSSOC,
166166
time.Now(),
167167
)
168+
return nil
168169
}
169170

170-
func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval time.Duration, dataDB *sql.DB, deviceID int, dryRun bool) {
171+
func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval time.Duration, dataDB *sql.DB, deviceID int, dryRun bool) error {
171172
if samples.IsEmpty() {
172173
s.logger.Printf("Data integration: no samples collected in period")
173-
return
174+
return nil
174175
}
175176

176177
data := samples.IntegrateSamples(pollInterval)
177178
timestamp := time.Now()
178179

179180
if dataDB == nil {
180-
return
181+
return nil
181182
}
182183

183184
// Fetch weather data from meteo API
@@ -205,18 +206,18 @@ func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval t
205206
if found && spotPrice > 0 {
206207
// Import cost: (spot price + operator fee + delivery fee) * energy in MWh
207208
importPricePerMWh := spotPrice + config.ImportPriceOperatorFee + config.ImportPriceDeliveryFee
208-
gridImportCost = (importPricePerMWh / 1000.0) * data.gridImportPower // Convert to EUR
209+
gridImportCost = (importPricePerMWh / 1000.0) * data.gridImportPower
209210

210211
// Export revenue (negative cost): (spot price - operator fee) * energy in MWh
211212
exportPricePerMWh := spotPrice - config.ExportPriceOperatorFee
212-
gridExportCost = (exportPricePerMWh / 1000.0) * data.gridExportPower // Convert to EUR
213+
gridExportCost = (exportPricePerMWh / 1000.0) * data.gridExportPower
213214
}
214215
}
215216

216217
if dryRun {
217218
// DRY-RUN MODE: Log the action without saving to database
218219
s.logger.Printf("Data integration [DRY-RUN]: would save metrics for device_id=%d at %s", deviceID, timestamp.Format(time.RFC3339))
219-
s.logger.Printf(" PV: %.3f kWh, Grid Import: %.3f kWh (€%.3f), Grid Export: %.3f kWh (€%.3f)",
220+
s.logger.Printf(" PV: %.3f kWh, Grid Import: %.3f kWh (%.3f), Grid Export: %.3f kWh (%.3f)",
220221
data.pvTotalPower, data.gridImportPower, gridImportCost, data.gridExportPower, gridExportCost)
221222
s.logger.Printf(" Battery Charge: %.3f kWh, Battery Discharge: %.3f kWh, SOC: %.1f%%",
222223
data.batteryChargePower, data.batteryDischargePower, data.batterySoc)
@@ -247,16 +248,17 @@ func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval t
247248
)
248249
if err != nil {
249250
s.logger.Printf("Data integration: failed to insert metrics: %v", err)
250-
return
251+
return nil
251252
}
252253

253254
s.logger.Printf("Data integration: saved metrics for device_id=%d at %s", deviceID, timestamp.Format(time.RFC3339))
254-
s.logger.Printf(" PV: %.3f kWh, Grid Import: %.3f kWh (€%.3f), Grid Export: %.3f kWh (€%.3f)",
255+
s.logger.Printf(" PV: %.3f kWh, Grid Import: %.3f kWh (%.3f), Grid Export: %.3f kWh (%.3f)",
255256
data.pvTotalPower, data.gridImportPower, gridImportCost, data.gridExportPower, gridExportCost)
256257
s.logger.Printf(" Battery Charge: %.3f kWh, Battery Discharge: %.3f kWh, SOC: %.1f%%",
257258
data.batteryChargePower, data.batteryDischargePower, data.batterySoc)
258259
s.logger.Printf(" EV Charge: %.3f kWh, Load: %.3f kWh", data.evdcChargePower, data.loadPower)
259260
}
261+
return nil
260262
}
261263

262264
func (s *MinerScheduler) fetchCloudCoverage() (*float64, error) {

‎scheduler/miners.go‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,16 @@ func (s *MinerScheduler) discoverMiners(ctx context.Context) error {
4242
}
4343

4444
// RunMinerDiscovery runs the miner discovery process as a scheduled task
45-
func (s *MinerScheduler) RunMinerDiscovery(ctx context.Context) {
45+
func (s *MinerScheduler) RunMinerDiscovery(ctx context.Context) error {
4646
s.logger.Printf("Starting miner discovery task at %s", time.Now().Format(time.RFC3339))
4747

4848
if err := s.discoverMiners(ctx); err != nil {
4949
s.logger.Printf("Error discovering miners: %v", err)
50-
return
50+
return err
5151
}
5252

5353
s.logger.Printf("Miner discovery task completed successfully")
54+
return nil
5455
}
5556

5657
// getMinerPowerConsumption returns the power consumption in kW for a given miner state and work mode
@@ -301,10 +302,10 @@ func (s *MinerScheduler) controlMiner(m *miners.AvalonQHost, totalPower float64,
301302
}
302303

303304
// runStateCheck executes the state monitoring task for miners
304-
func (s *MinerScheduler) runStateCheck(ctx context.Context) {
305+
func (s *MinerScheduler) runStateCheck(ctx context.Context) error {
305306
minersList := s.refreshMinersState(ctx)
306307
if len(minersList) == 0 {
307-
return
308+
return nil
308309
}
309310

310311
isDryRun := s.config.DryRun
@@ -409,5 +410,7 @@ func (s *MinerScheduler) runStateCheck(ctx context.Context) {
409410
for _, err := range errors {
410411
s.logger.Printf(" - %v", err)
411412
}
413+
return fmt.Errorf("errors during state check")
412414
}
415+
return nil
413416
}

‎scheduler/mpc.go‎

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@ import (
1313
)
1414

1515
// RunMPCOptimize executes the MPC optimization task
16-
func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) {
16+
func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) error {
1717
s.logger.Printf("Starting MPC optimization task at %s", time.Now().Format(time.RFC3339))
1818

1919
config := s.GetConfig()
2020

2121
// Check if Plant Modbus Address is configured
2222
if config.PlantModbusAddress == "" {
2323
s.logger.Printf("MPC optimization skipped: PlantModbusAddress not configured")
24-
return
24+
return nil
2525
}
2626

2727
// Step 1: Read plant running info from inverter
2828
plantInfo, err := s.readPlantRunningInfo(config)
2929
if err != nil {
3030
s.logger.Printf("Error reading plant running info from inverter: %v", err)
31-
return
31+
return err
3232
}
3333

3434
// Extract initial SOC from plant info
@@ -39,12 +39,12 @@ func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) {
3939
forecast, err := s.buildMPCForecast(ctx, config, plantInfo)
4040
if err != nil {
4141
s.logger.Printf("Error building MPC forecast: %v", err)
42-
return
42+
return err
4343
}
4444

4545
if len(forecast) == 0 {
4646
s.logger.Printf("No forecast data available for MPC optimization")
47-
return
47+
return nil
4848
}
4949

5050
s.logger.Printf("Built forecast with %d time slots", len(forecast))
@@ -69,7 +69,7 @@ func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) {
6969
decisions := controller.Optimize(forecast)
7070
if len(decisions) == 0 {
7171
s.logger.Printf("MPC optimization produced no decisions")
72-
return
72+
return nil
7373
}
7474

7575
// Step 5: Save optimization results to memory
@@ -110,10 +110,11 @@ func (s *MinerScheduler) RunMPCOptimize(ctx context.Context) {
110110

111111
if err != nil {
112112
s.logger.Printf("Error executing MPC decision: %v (will retry every minute)", err)
113-
return
113+
return err
114114
}
115115

116116
s.logger.Printf("MPC optimization task completed successfully")
117+
return nil
117118
}
118119

119120
// readPlantRunningInfo reads the plant running information from the inverter
@@ -536,21 +537,15 @@ func (s *MinerScheduler) executeMPCDecision(decision *mpc.ControlDecision, dryRu
536537

537538
// runMPCExecution re-executes the current MPC decision only if previous execution failed
538539
// This ensures the decision is applied even if previous execution failed
539-
func (s *MinerScheduler) runMPCExecution() {
540+
func (s *MinerScheduler) runMPCExecution() error {
540541

541542
s.mu.RLock()
542543
config := s.GetConfig()
543544

544-
// Check if Plant Modbus Address is configured
545-
if config.PlantModbusAddress == "" {
545+
// Check if Plant Modbus Address is configured and there are decisions
546+
if config.PlantModbusAddress == "" || len(s.mpcDecisions) == 0 {
546547
s.mu.RUnlock()
547-
return
548-
}
549-
550-
// Get current MPC decision based on current timestamp
551-
if len(s.mpcDecisions) == 0 {
552-
s.mu.RUnlock()
553-
return
548+
return nil
554549
}
555550

556551
now := time.Now().Unix()
@@ -570,7 +565,7 @@ func (s *MinerScheduler) runMPCExecution() {
570565
if currentDecision == nil {
571566
// No matching decision found for current timestamp
572567
s.mu.RUnlock()
573-
return
568+
return nil
574569
}
575570

576571
lastExecuted := s.lastExecutedDecision
@@ -579,7 +574,7 @@ func (s *MinerScheduler) runMPCExecution() {
579574
// Check if this decision has already been executed
580575
if lastExecuted != nil && currentDecision.Timestamp == lastExecuted.Timestamp {
581576
// Decision already executed, no need to retry
582-
return
577+
return nil
583578
}
584579

585580
s.logger.Printf("Executing MPC decision for timestamp %d (hour %d)", currentDecision.Timestamp, currentDecision.Hour)
@@ -593,12 +588,13 @@ func (s *MinerScheduler) runMPCExecution() {
593588
s.lastExecutedDecision = nil
594589
s.mu.Unlock()
595590
s.logger.Printf("Error executing MPC decision: %v (will retry again in 1 minute)", err)
596-
return
591+
return err
597592
}
598593

599594
// Execution succeeded, store the executed decision
600595
s.lastExecutedDecision = currentDecision
601596
s.mu.Unlock()
602597

603598
s.logger.Printf("Successfully executed MPC decision")
599+
return nil
604600
}

0 commit comments

Comments
 (0)