Metrics: rename import/export to energy/returnEnergy (#29907)

This commit is contained in:
andig 2026-05-15 14:44:11 +02:00 • committed by GitHub
parent fd1b63c015
commit db82557423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 87 additions and 79 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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,
})
}

View file

@ -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')

View file

@ -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) {

View file

@ -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
}
}

View file

@ -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

View file

@ -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);
});
});