diff --git a/core/metrics/accumulator.go b/core/metrics/accumulator.go index c1a109429..334104d88 100644 --- a/core/metrics/accumulator.go +++ b/core/metrics/accumulator.go @@ -9,12 +9,12 @@ import ( ) type Accumulator struct { - clock clock.Clock - updated time.Time - importMeter *float64 // kWh - exportMeter *float64 // kWh - Import float64 `json:"import"` // kWh - Export float64 `json:"export"` // kWh + clock clock.Clock + updated time.Time + importMeter *float64 // kWh + exportMeter *float64 // kWh + Energy float64 `json:"energy"` // kWh + ReturnEnergy float64 `json:"returnEnergy"` // kWh } func WithClock(clock clock.Clock) func(*Accumulator) { @@ -37,7 +37,7 @@ func (m *Accumulator) Updated() time.Time { func (m *Accumulator) String() string { b := new(bytes.Buffer) - fmt.Fprintf(b, "Accumulated: %.3fkWh pos, %.3fkWh neg, updated: %v", m.Import, m.Export, m.updated.Truncate(time.Second)) + fmt.Fprintf(b, "Accumulated: %.3fkWh pos, %.3fkWh neg, updated: %v", m.Energy, m.ReturnEnergy, m.updated.Truncate(time.Second)) if m.importMeter != nil || m.exportMeter != nil { fmt.Fprintf(b, " meter: ") if m.importMeter != nil { @@ -52,12 +52,12 @@ func (m *Accumulator) String() string { // Imported returns the accumulated import energy in kWh func (m *Accumulator) Imported() float64 { - return m.Import + return m.Energy } // Exported returns the accumulated export energy in kWh func (m *Accumulator) Exported() float64 { - return m.Export + return m.ReturnEnergy } // SetImportMeterTotal adds the difference to the last total meter value in kWh @@ -72,7 +72,7 @@ func (m *Accumulator) SetImportMeterTotal(v float64) { } if v >= *m.importMeter { - m.Import += v - *m.importMeter + m.Energy += v - *m.importMeter } } @@ -88,7 +88,7 @@ func (m *Accumulator) SetExportMeterTotal(v float64) { } if v >= *m.exportMeter { - m.Export += v - *m.exportMeter + m.ReturnEnergy += v - *m.exportMeter } } @@ -100,7 +100,7 @@ func (m *Accumulator) AddImportEnergy(v float64) { return } - m.Import += v + m.Energy += v } // AddExportEnergy adds the given energy in kWh to the negative meter @@ -111,7 +111,7 @@ func (m *Accumulator) AddExportEnergy(v float64) { return } - m.Export += v + m.ReturnEnergy += v } // AddPower adds the given power in W, calculating the energy based on the time since the last update diff --git a/core/metrics/collector.go b/core/metrics/collector.go index 50d285aa8..4814f6774 100644 --- a/core/metrics/collector.go +++ b/core/metrics/collector.go @@ -64,8 +64,8 @@ func (c *Collector) process(fun func()) error { } c.started = slotStart - c.accu.Import = 0 - c.accu.Export = 0 + c.accu.Energy = 0 + c.accu.ReturnEnergy = 0 } return nil diff --git a/core/metrics/db.go b/core/metrics/db.go index daed84dbd..bbaf6b3fb 100644 --- a/core/metrics/db.go +++ b/core/metrics/db.go @@ -12,11 +12,11 @@ import ( ) type meter struct { - Meter int `json:"meter" gorm:"column:meter;uniqueIndex:meters_meter_ts"` - Timestamp int64 `json:"ts" gorm:"column:ts;uniqueIndex:meters_meter_ts"` // start of 15min slot - Entity entity `json:"-" gorm:"foreignkey:Meter;references:Id"` - Import float64 `json:"import" gorm:"column:import"` - Export float64 `json:"export" gorm:"column:export"` + Meter int `json:"meter" gorm:"column:meter;uniqueIndex:meters_meter_ts"` + Timestamp int64 `json:"ts" gorm:"column:ts;uniqueIndex:meters_meter_ts"` // start of 15min slot + Entity entity `json:"-" gorm:"foreignkey:Meter;references:Id"` + Energy float64 `json:"energy" gorm:"column:energy"` + ReturnEnergy float64 `json:"returnEnergy" gorm:"column:return_energy"` } type entity struct { @@ -86,6 +86,14 @@ func SetupSchema() error { return err } + // meter: rename to energy/return_energy + if err := rename("import", "energy"); err != nil { + return err + } + if err := rename("export", "return_energy"); err != nil { + return err + } + // meter: ts migration if m.HasTable(new(meter)) { types, err := m.ColumnTypes(new(meter)) @@ -115,11 +123,11 @@ func SetupSchema() error { } // persist stores 15min consumption in kWh -func persist(entity entity, ts time.Time, imp, exp float64) error { +func persist(entity entity, ts time.Time, energy, returnEnergy float64) error { return db.Instance.Create(&meter{ - Meter: entity.Id, - Timestamp: ts.Truncate(tariff.SlotDuration).Unix(), - Import: imp, - Export: exp, + Meter: entity.Id, + Timestamp: ts.Truncate(tariff.SlotDuration).Unix(), + Energy: energy, + ReturnEnergy: returnEnergy, }).Error } diff --git a/core/metrics/db_history.go b/core/metrics/db_history.go index 3ee8a6d2f..eedb638ef 100644 --- a/core/metrics/db_history.go +++ b/core/metrics/db_history.go @@ -10,10 +10,10 @@ import ( // Slot represents an aggregated energy time slot type Slot struct { - Start time.Time `json:"start"` - End time.Time `json:"end"` - Import float64 `json:"import"` - Export float64 `json:"export"` + Start time.Time `json:"start"` + End time.Time `json:"end"` + Energy float64 `json:"energy"` + ReturnEnergy float64 `json:"returnEnergy"` } // Series represents a named series of energy slots @@ -37,8 +37,8 @@ var aggregateDurations = map[string]func(time.Time) time.Time{ "month": func(t time.Time) time.Time { return t.AddDate(0, 1, 0) }, } -// QueryImportEnergy returns aggregated energy data, per entity or per group. -func QueryImportEnergy(from, to time.Time, aggregate string, grouped bool) ([]Series, error) { +// QueryEnergy returns aggregated energy data, per entity or per group. +func QueryEnergy(from, to time.Time, aggregate string, grouped bool) ([]Series, error) { addDuration := aggregateDurations[aggregate] format, ok := aggregateFormats[aggregate] @@ -52,18 +52,18 @@ func QueryImportEnergy(from, to time.Time, aggregate string, grouped bool) ([]Se } type row struct { - Name string - Group string - Start SqlTime - Import float64 - Export float64 + Name string + Group string + Start SqlTime + Energy float64 + ReturnEnergy float64 } tx := db.Instance.Table("meters m"). Select(`e.name, e."group", MIN(m.ts) AS start, - COALESCE(SUM(m."import"), 0) AS import, - COALESCE(SUM(m.export), 0) AS export`). + COALESCE(SUM(m.energy), 0) AS energy, + COALESCE(SUM(m.return_energy), 0) AS return_energy`). Joins("JOIN entities e ON m.meter = e.id"). Group(groupCols). Order(groupCols) @@ -93,10 +93,10 @@ func QueryImportEnergy(from, to time.Time, aggregate string, grouped bool) ([]Se s := &res[len(res)-1] s.Data = append(s.Data, Slot{ - Start: time.Time(r.Start), - End: addDuration(time.Time(r.Start)), - Import: r.Import, - Export: r.Export, + Start: time.Time(r.Start), + End: addDuration(time.Time(r.Start)), + Energy: r.Energy, + ReturnEnergy: r.ReturnEnergy, }) } diff --git a/core/metrics/db_profile.go b/core/metrics/db_profile.go index 217663f85..0130986db 100644 --- a/core/metrics/db_profile.go +++ b/core/metrics/db_profile.go @@ -18,7 +18,7 @@ func importProfile(entity entity, from time.Time) (*[96]float64, error) { return nil, err } - rows, err := db.Query(`SELECT min(ts) AS ts, avg(import) AS import + rows, err := db.Query(`SELECT min(ts) AS ts, avg(energy) AS energy FROM meters WHERE meter = ? AND ts >= ? GROUP BY strftime("%H:%M", ts, 'unixepoch', 'localtime') diff --git a/core/metrics/db_test.go b/core/metrics/db_test.go index e3d1e8d30..8d796dff5 100644 --- a/core/metrics/db_test.go +++ b/core/metrics/db_test.go @@ -44,7 +44,7 @@ func TestSqliteTimestamp(t *testing.T) { require.True(t, clock.Now().Equal(time.Time(ts)), "expected %v, got %v", clock.Now().Local(), time.Time(ts).Local()) } -func TestQueryImportEnergyUTCFilter(t *testing.T) { +func TestQueryEnergyUTCFilter(t *testing.T) { require.NoError(t, db.NewInstance("sqlite", ":memory:")) require.NoError(t, SetupSchema()) @@ -62,15 +62,15 @@ func TestQueryImportEnergyUTCFilter(t *testing.T) { from := base.Add(-time.Hour).UTC() to := base.Add(3 * time.Hour).UTC() - res, err := QueryImportEnergy(from, to, "hour", false) + res, err := QueryEnergy(from, to, "hour", false) require.NoError(t, err) require.Len(t, res, 1) require.Len(t, res[0].Data, 2) - require.InDelta(t, 1, res[0].Data[0].Export, 0.001) - require.InDelta(t, 2, res[0].Data[1].Export, 0.001) + require.InDelta(t, 1, res[0].Data[0].ReturnEnergy, 0.001) + require.InDelta(t, 2, res[0].Data[1].ReturnEnergy, 0.001) } -func TestQueryImportEnergyGrouped(t *testing.T) { +func TestQueryEnergyGrouped(t *testing.T) { require.NoError(t, db.NewInstance("sqlite", ":memory:")) require.NoError(t, SetupSchema()) @@ -92,22 +92,22 @@ func TestQueryImportEnergyGrouped(t *testing.T) { to := base.Add(3 * time.Hour).UTC() // ungrouped: 2 series - res, err := QueryImportEnergy(from, to, "hour", false) + res, err := QueryEnergy(from, to, "hour", false) require.NoError(t, err) require.Len(t, res, 2) // grouped: 1 series, values summed per bucket - res, err = QueryImportEnergy(from, to, "hour", true) + res, err = QueryEnergy(from, to, "hour", true) require.NoError(t, err) require.Len(t, res, 1) require.Equal(t, Grid, res[0].Group) require.Empty(t, res[0].Name) require.Len(t, res[0].Data, 2) - require.InDelta(t, 1+2, res[0].Data[0].Import, 0.001) - require.InDelta(t, 3+4, res[0].Data[1].Import, 0.001) + require.InDelta(t, 1+2, res[0].Data[0].Energy, 0.001) + require.InDelta(t, 3+4, res[0].Data[1].Energy, 0.001) } -func TestQueryImportEnergyMultipleSeries(t *testing.T) { +func TestQueryEnergyMultipleSeries(t *testing.T) { require.NoError(t, db.NewInstance("sqlite", ":memory:")) require.NoError(t, SetupSchema()) @@ -134,7 +134,7 @@ func TestQueryImportEnergyMultipleSeries(t *testing.T) { to := base.Add(3 * time.Hour).UTC() // ungrouped: 3 series, each with 2 slots - res, err := QueryImportEnergy(from, to, "hour", false) + res, err := QueryEnergy(from, to, "hour", false) require.NoError(t, err) require.Len(t, res, 3) @@ -147,13 +147,13 @@ func TestQueryImportEnergyMultipleSeries(t *testing.T) { require.Equal(t, PV, byName["pv1"].Group) require.Equal(t, PV, byName["pv2"].Group) - require.InDelta(t, 1, byName[Grid].Data[0].Import, 0.001) - require.InDelta(t, 2, byName[Grid].Data[1].Import, 0.001) - require.InDelta(t, 10, byName["pv1"].Data[0].Export, 0.001) - require.InDelta(t, 21, byName["pv2"].Data[1].Export, 0.001) + require.InDelta(t, 1, byName[Grid].Data[0].Energy, 0.001) + require.InDelta(t, 2, byName[Grid].Data[1].Energy, 0.001) + require.InDelta(t, 10, byName["pv1"].Data[0].ReturnEnergy, 0.001) + require.InDelta(t, 21, byName["pv2"].Data[1].ReturnEnergy, 0.001) // grouped: 2 series, pv summed per bucket - res, err = QueryImportEnergy(from, to, "hour", true) + res, err = QueryEnergy(from, to, "hour", true) require.NoError(t, err) require.Len(t, res, 2) @@ -166,10 +166,10 @@ func TestQueryImportEnergyMultipleSeries(t *testing.T) { require.Contains(t, byGroup, Grid) require.Contains(t, byGroup, PV) - require.InDelta(t, 1, byGroup[Grid].Data[0].Import, 0.001) - require.InDelta(t, 2, byGroup[Grid].Data[1].Import, 0.001) - require.InDelta(t, 10+20, byGroup[PV].Data[0].Export, 0.001) - require.InDelta(t, 11+21, byGroup[PV].Data[1].Export, 0.001) + require.InDelta(t, 1, byGroup[Grid].Data[0].Energy, 0.001) + require.InDelta(t, 2, byGroup[Grid].Data[1].Energy, 0.001) + require.InDelta(t, 10+20, byGroup[PV].Data[0].ReturnEnergy, 0.001) + require.InDelta(t, 11+21, byGroup[PV].Data[1].ReturnEnergy, 0.001) } func TestUpdateProfile(t *testing.T) { diff --git a/core/site_tariffs.go b/core/site_tariffs.go index da3684894..50fdbe74f 100644 --- a/core/site_tariffs.go +++ b/core/site_tariffs.go @@ -165,7 +165,7 @@ func (site *Site) solarDetails(solar api.Rates) solarDetails { // to the forecast. Returns 1.0 when not enough data is available to make // the ratio meaningful. func (site *Site) solarScale() float64 { - series, err := metrics.QueryImportEnergy(now.BeginningOfDay(), time.Now(), "day", true) + series, err := metrics.QueryEnergy(now.BeginningOfDay(), time.Now(), "day", true) if err != nil { site.log.ERROR.Printf("solar forecast scale: %v", err) return 1 @@ -178,9 +178,9 @@ func (site *Site) solarScale() float64 { } switch s.Group { case metrics.PV: - pv = s.Data[0].Import + pv = s.Data[0].Energy case metrics.Forecast: - fcst = s.Data[0].Import + fcst = s.Data[0].Energy } } diff --git a/server/http_history_handler.go b/server/http_history_handler.go index 415688bba..c426017fc 100644 --- a/server/http_history_handler.go +++ b/server/http_history_handler.go @@ -43,7 +43,7 @@ func energyHistoryHandler(w http.ResponseWriter, r *http.Request) { grouped := q.Get("grouped") == "true" - res, err := metrics.QueryImportEnergy(from, to, aggregate, grouped) + res, err := metrics.QueryEnergy(from, to, aggregate, grouped) if err != nil { jsonError(w, http.StatusInternalServerError, err) return diff --git a/tests/energy-history.spec.ts b/tests/energy-history.spec.ts index 662a5ffe0..2d484e834 100644 --- a/tests/energy-history.spec.ts +++ b/tests/energy-history.spec.ts @@ -31,10 +31,10 @@ test.describe("energy history API", () => { expect(grid.data).toHaveLength(6); expect(home.data).toHaveLength(6); - expect(home.data[0].import).toBeCloseTo(0.1, 4); - expect(home.data[0].export).toBeCloseTo(0, 4); - expect(grid.data[0].import).toBeCloseTo(0.5, 4); - expect(grid.data[0].export).toBeCloseTo(0.1, 4); + expect(home.data[0].energy).toBeCloseTo(0.1, 4); + expect(home.data[0].returnEnergy).toBeCloseTo(0, 4); + expect(grid.data[0].energy).toBeCloseTo(0.5, 4); + expect(grid.data[0].returnEnergy).toBeCloseTo(0.1, 4); }); test("day aggregation", async ({ request }) => { @@ -58,15 +58,15 @@ test.describe("energy history API", () => { const [gridDay1, gridDay2] = grid.data; // day 1 (2026-03-24): 4 slots - expect(homeDay1.import).toBeCloseTo(0.4, 4); - expect(homeDay1.export).toBeCloseTo(0, 4); - expect(gridDay1.import).toBeCloseTo(2.0, 4); - expect(gridDay1.export).toBeCloseTo(0.4, 4); + expect(homeDay1.energy).toBeCloseTo(0.4, 4); + expect(homeDay1.returnEnergy).toBeCloseTo(0, 4); + expect(gridDay1.energy).toBeCloseTo(2.0, 4); + expect(gridDay1.returnEnergy).toBeCloseTo(0.4, 4); // day 2 (2026-03-25): 2 slots - expect(homeDay2.import).toBeCloseTo(0.2, 4); - expect(homeDay2.export).toBeCloseTo(0, 4); - expect(gridDay2.import).toBeCloseTo(1.0, 4); - expect(gridDay2.export).toBeCloseTo(0.2, 4); + expect(homeDay2.energy).toBeCloseTo(0.2, 4); + expect(homeDay2.returnEnergy).toBeCloseTo(0, 4); + expect(gridDay2.energy).toBeCloseTo(1.0, 4); + expect(gridDay2.returnEnergy).toBeCloseTo(0.2, 4); }); });