Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 27 additions & 20 deletions scheduler/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ func (w *WeatherForecastCache) Set(forecast *meteo.METJSONForecast) {

// DataSample represents a single measurement of power and battery data.
type DataSample struct {
pvPower float64
gridPower float64 // positive = import, negative = export
batteryPower float64 // positive = charging, negative = discharging
evdcPower float64
batterySoc float64 // %
ts time.Time
pvPower float64
gridPower float64 // positive = import, negative = export
batteryPower float64 // positive = charging, negative = discharging
evdcPower float64
batterySoc float64 // %
batteryAvgCellTemp float64 // °C
ts time.Time
}

// DataSamples is a thread-safe collection of power measurement samples.
Expand All @@ -59,16 +60,17 @@ type DataSamples struct {
}

// AddSample adds a new power measurement sample to the collection.
func (d *DataSamples) AddSample(pvPower, gridPower, batteryPower, evdcPower, batterySoc float64, ts time.Time) {
func (d *DataSamples) AddSample(pvPower, gridPower, batteryPower, evdcPower, batterySoc, batteryAvgCellTemp float64, ts time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
d.samples = append(d.samples, DataSample{
pvPower: pvPower,
gridPower: gridPower,
batteryPower: batteryPower,
evdcPower: evdcPower,
batterySoc: batterySoc,
ts: ts,
pvPower: pvPower,
gridPower: gridPower,
batteryPower: batteryPower,
evdcPower: evdcPower,
batterySoc: batterySoc,
batteryAvgCellTemp: batteryAvgCellTemp,
ts: ts,
})
}

Expand All @@ -82,6 +84,7 @@ type IntegratedData struct {
evdcChargePower float64
loadPower float64
batterySoc float64
batteryAvgCellTemp float64
timestamp time.Time
sampleCount int // Number of samples integrated
}
Expand Down Expand Up @@ -125,8 +128,9 @@ func (d *DataSamples) IntegrateSamples(pollInterval time.Duration, cutoffTime ti
// EV DC charging power
result.evdcChargePower += sample.evdcPower * energyKWh

// Keep the last battery SOC
// Keep the last battery SOC and temperature
result.batterySoc = sample.batterySoc
result.batteryAvgCellTemp = sample.batteryAvgCellTemp
}

// Calculate load: Load = PV + Battery Discharge + Grid Import - Battery Charge - Grid Export - EV Charge
Expand Down Expand Up @@ -190,6 +194,7 @@ func (s *MinerScheduler) runDataPoll(samples *DataSamples) error {
info.ESSPower,
info.DCChargerOutputPower,
info.ESSSOC,
info.ESSAvgCellTemperature,
time.Now(),
)
return nil
Expand Down Expand Up @@ -258,8 +263,8 @@ func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval t
deviceID, timestamp.Format(time.RFC3339), data.sampleCount)
s.logger.Printf(" PV: %.3f kWh, Grid Import: %.3f kWh (%.3f), Grid Export: %.3f kWh (%.3f)",
data.pvTotalPower, data.gridImportPower, gridImportCost, data.gridExportPower, gridExportCost)
s.logger.Printf(" Battery Charge: %.3f kWh, Battery Discharge: %.3f kWh, SOC: %.1f%%",
data.batteryChargePower, data.batteryDischargePower, data.batterySoc)
s.logger.Printf(" Battery Charge: %.3f kWh, Battery Discharge: %.3f kWh, SOC: %.1f%%, Temp: %.1f°C",
data.batteryChargePower, data.batteryDischargePower, data.batterySoc, data.batteryAvgCellTemp)
s.logger.Printf(" EV Charge: %.3f kWh, Load: %.3f kWh", data.evdcChargePower, data.loadPower)
if cloudCoverage != nil {
s.logger.Printf(" Cloud Coverage: %.1f%%", *cloudCoverage)
Expand All @@ -277,14 +282,16 @@ func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval t
grid_export_power, grid_import_power,
battery_charge_power, battery_discharge_power, battery_soc,
evdc_charge_power, load_power,
grid_export_cost, grid_import_cost
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
grid_export_cost, grid_import_cost,
battery_avg_cell_temperature
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,
timestamp, deviceID, "energy_flow",
data.pvTotalPower, cloudCoverage, weatherSymbol,
data.gridExportPower, data.gridImportPower,
data.batteryChargePower, data.batteryDischargePower, data.batterySoc,
data.evdcChargePower, data.loadPower,
gridExportCost, gridImportCost,
data.batteryAvgCellTemp,
)
if err != nil {
s.logger.Printf("Data integration: failed to insert metrics: %v", err)
Expand All @@ -298,8 +305,8 @@ func (s *MinerScheduler) runDataIntegration(samples *DataSamples, pollInterval t
deviceID, timestamp.Format(time.RFC3339), data.sampleCount)
s.logger.Printf(" PV: %.3f kWh, Grid Import: %.3f kWh (%.3f), Grid Export: %.3f kWh (%.3f)",
data.pvTotalPower, data.gridImportPower, gridImportCost, data.gridExportPower, gridExportCost)
s.logger.Printf(" Battery Charge: %.3f kWh, Battery Discharge: %.3f kWh, SOC: %.1f%%",
data.batteryChargePower, data.batteryDischargePower, data.batterySoc)
s.logger.Printf(" Battery Charge: %.3f kWh, Battery Discharge: %.3f kWh, SOC: %.1f%%, Temp: %.1f°C",
data.batteryChargePower, data.batteryDischargePower, data.batterySoc, data.batteryAvgCellTemp)
s.logger.Printf(" EV Charge: %.3f kWh, Load: %.3f kWh", data.evdcChargePower, data.loadPower)
}
return nil
Expand Down
16 changes: 9 additions & 7 deletions scheduler/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ func TestDataSamples_IntegrateSamplesWithPeriodBoundary(t *testing.T) {
// 6 samples: 0s, 10s, 20s, 30s, 40s, 50s
for i := range 6 {
ts := baseTime.Add(time.Duration(i) * pollInterval)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, ts)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, 25.0, ts)
}

// Add samples for second period (12:01:00 to 12:01:50)
// 6 samples: 60s, 70s, 80s, 90s, 100s, 110s
for i := range 6 {
ts := baseTime.Add(integrationPeriod).Add(time.Duration(i) * pollInterval)
samples.AddSample(200.0, 100.0, 60.0, 20.0, 75.0, ts)
samples.AddSample(200.0, 100.0, 60.0, 20.0, 75.0, 26.0, ts)
}

// Integrate first period only (cutoff at 12:01:00)
Expand Down Expand Up @@ -64,7 +64,7 @@ func TestDataSamples_ClearBefore(t *testing.T) {
// 12 samples: 0s, 10s, 20s, 30s, 40s, 50s, 60s, 70s, 80s, 90s, 100s, 110s
for i := range 12 {
ts := baseTime.Add(time.Duration(i) * 10 * time.Second)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, ts)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, 25.0, ts)
}

// Clear samples before and at 1 minute mark (60s)
Expand Down Expand Up @@ -96,7 +96,7 @@ func TestDataSamples_IntegrationPreservesForRetry(t *testing.T) {
// Add 6 samples for first period (0s to 50s)
for i := range 6 {
ts := baseTime.Add(time.Duration(i) * pollInterval)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, ts)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, 25.0, ts)
}

// Cutoff at 50s (not 60s) to avoid including second period samples
Expand All @@ -110,7 +110,7 @@ func TestDataSamples_IntegrationPreservesForRetry(t *testing.T) {
// Add more samples for second period (60s to 110s)
for i := range 6 {
ts := baseTime.Add(60 * time.Second).Add(time.Duration(i) * pollInterval)
samples.AddSample(200.0, 100.0, 60.0, 20.0, 75.0, ts)
samples.AddSample(200.0, 100.0, 60.0, 20.0, 75.0, 26.0, ts)
}

// Retry integration for first period (same cutoff)
Expand Down Expand Up @@ -181,10 +181,10 @@ func TestDataSamples_BoundaryConditions(t *testing.T) {
baseTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)

// Add sample exactly at boundary
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, baseTime)
samples.AddSample(100.0, 50.0, 30.0, 10.0, 80.0, 25.0, baseTime)

// Add sample 1 nanosecond after boundary
samples.AddSample(200.0, 100.0, 60.0, 20.0, 75.0, baseTime.Add(1))
samples.AddSample(200.0, 100.0, 60.0, 20.0, 75.0, 26.0, baseTime.Add(1))

// Integrate with cutoff at baseTime
data := samples.IntegrateSamples(pollInterval, baseTime)
Expand Down Expand Up @@ -221,6 +221,7 @@ func TestIntegratedData_EnergyCalculations(t *testing.T) {
300.0, // Battery charge: 300W
100.0, // EVDC: 100W
85.0, // SOC: 85%
25.0, // Battery temp: 25°C
baseTime,
)

Expand All @@ -230,6 +231,7 @@ func TestIntegratedData_EnergyCalculations(t *testing.T) {
-150.0, // Battery discharge: 150W
0.0, // EVDC: 0W
83.0, // SOC: 83%
26.0, // Battery temp: 26°C
baseTime.Add(pollInterval),
)

Expand Down
1 change: 1 addition & 0 deletions sigenergy/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func ShowPlantInfo(plantModbusAddress string) error {
fmt.Println("--------------------------------------------------")
fmt.Printf(" ESS SOC: %.1f %%\n", info.ESSSOC)
fmt.Printf(" ESS SOH: %.1f %%\n", info.ESSSOH)
fmt.Printf(" ESS Average Cell Temperature: %.1f °C\n", info.ESSAvgCellTemperature)
fmt.Printf(" ESS Rated Energy Capacity: %.2f kWh\n", info.ESSRatedEnergyCapacity)
fmt.Printf(" ESS Charge Off SOC: %.1f %%\n", info.ESSChargeOffSOC)
fmt.Printf(" ESS Discharge Off SOC: %.1f %%\n", info.ESSDischargeOffSOC)
Expand Down
11 changes: 11 additions & 0 deletions sigenergy/modbus_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ type PlantRunningInfo struct {
ESSChargeOffSOC float64 // %
ESSDischargeOffSOC float64 // %
ESSSOH float64 // %
ESSAvgCellTemperature float64 // °C
DCChargerOutputPower float64 // kW
DCChargerVehicleSOC float64 // %
}
Expand Down Expand Up @@ -217,6 +218,16 @@ func (c *SigenModbusClient) ReadPlantRunningInfo() (*PlantRunningInfo, error) {
info.DCChargerVehicleSOC = float64(bytesToU16(data3[4:6])) / 10.0
}

// Read ESS Average Cell Temperature from first inverter (slave address 1, register 30603)
// Note: This assumes at least one hybrid inverter is present with slave ID 1
c.SetSlaveID(1)
data4, err := c.client.ReadInputRegisters(30603, 1)
if err == nil {
info.ESSAvgCellTemperature = float64(bytesToS16(data4[0:2])) / 10.0
}
// Reset to plant address
c.SetSlaveID(PlantAddress)

return info, nil
}

Expand Down
3 changes: 3 additions & 0 deletions sql/metrics.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ CREATE TABLE metrics (
grid_export_cost NUMERIC,
grid_import_cost NUMERIC,
battery_soc NUMERIC,
battery_avg_cell_temperature NUMERIC,
weather_symbol VARCHAR(100),
PRIMARY KEY (timestamp, device_id, metric_name)
);
Expand All @@ -36,6 +37,7 @@ CREATE INDEX idx_metrics_time ON metrics USING BRIN (timestamp);
-- grid_export_cost: Total cost of power exported to grid in EUR
-- grid_import_cost: Total cost of power imported from grid in EUR
-- battery_soc: Battery state of charge in % (snapshot at end of period)
-- battery_avg_cell_temperature: Battery average cell temperature in °C (snapshot at end of period)
-- weather_symbol: Weather condition symbol code (e.g., 'clearsky_day', 'rain', etc.)

-- Migration for existing tables:
Expand All @@ -48,4 +50,5 @@ CREATE INDEX idx_metrics_time ON metrics USING BRIN (timestamp);
-- ALTER TABLE metrics ADD COLUMN grid_export_cost NUMERIC;
-- ALTER TABLE metrics ADD COLUMN grid_import_cost NUMERIC;
-- ALTER TABLE metrics ADD COLUMN battery_soc NUMERIC;
-- ALTER TABLE metrics ADD COLUMN battery_avg_cell_temperature NUMERIC;
-- ALTER TABLE metrics ADD COLUMN weather_symbol VARCHAR(100);