144 lines
3.4 KiB
Go
144 lines
3.4 KiB
Go
package metrics
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/server/db"
|
|
"github.com/evcc-io/evcc/tariff"
|
|
)
|
|
|
|
const (
|
|
// groups
|
|
Forecast = "forecast"
|
|
Battery = "battery"
|
|
Grid = "grid"
|
|
PV = "pv"
|
|
Home = "home" // meter and group (virtual measurement)
|
|
Loadpoint = "loadpoint"
|
|
Meter = "meter" // generic meter (ext/aux)
|
|
)
|
|
|
|
type Collector struct {
|
|
entity entity
|
|
accu *Accumulator
|
|
started time.Time
|
|
}
|
|
|
|
func NewCollector(group, name, title string, opt ...func(*Accumulator)) (*Collector, error) {
|
|
entity, err := createEntity(group, name, title)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &Collector{
|
|
entity: entity,
|
|
accu: NewAccumulator(opt...),
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// createEntity ensures the entity row exists and refreshes its title.
|
|
func createEntity(group, name, title string) (entity, error) {
|
|
e := entity{Group: group, Name: name}
|
|
|
|
if err := db.Instance.Where(&e).Attrs(entity{Title: title}).FirstOrCreate(&e).Error; err != nil {
|
|
return e, err
|
|
}
|
|
|
|
return e, e.updateTitle(title)
|
|
}
|
|
|
|
// updateTitle refreshes the entity's stored title if it changed
|
|
func (e *entity) updateTitle(title string) error {
|
|
if title == "" || e.Title == title {
|
|
return nil
|
|
}
|
|
|
|
e.Title = title
|
|
return db.Instance.Model(e).UpdateColumn("title", title).Error
|
|
}
|
|
|
|
// UpdateTitle refreshes the collector entity's stored title if it changed.
|
|
func (c *Collector) UpdateTitle(title string) error {
|
|
return c.entity.updateTitle(title)
|
|
}
|
|
|
|
func (c *Collector) process(fun func()) error {
|
|
now := c.accu.clock.Now()
|
|
|
|
fun()
|
|
|
|
slotStart := now.Truncate(tariff.SlotDuration)
|
|
|
|
switch {
|
|
case c.started.IsZero():
|
|
// keep started un-truncated so a mid-slot start stays distinguishable
|
|
c.started = now
|
|
|
|
case slotStart.After(c.started):
|
|
// persist the completed slot only if started is the immediately
|
|
// preceding slot boundary - false for the mid-slot first slot and
|
|
// for a slot reached after a data gap
|
|
if c.started.Equal(slotStart.Add(-tariff.SlotDuration)) {
|
|
if err := c.persist(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
c.started = slotStart
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
c.accu.Energy = 0
|
|
c.accu.ReturnEnergy = 0
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Collector) persist() error {
|
|
return persist(c.entity, c.started, c.accu.Energy, c.accu.ReturnEnergy)
|
|
}
|
|
|
|
func (c *Collector) EnergyProfile(from time.Time) (*[96]float64, error) {
|
|
return energyProfile(c.entity, from)
|
|
}
|
|
|
|
func (c *Collector) SetEnergyMeterTotal(v float64) error {
|
|
return c.process(func() {
|
|
c.accu.SetEnergyMeterTotal(v)
|
|
})
|
|
}
|
|
|
|
func (c *Collector) SetReturnEnergyMeterTotal(v float64) error {
|
|
return c.process(func() {
|
|
c.accu.SetReturnEnergyMeterTotal(v)
|
|
})
|
|
}
|
|
|
|
// AddEnergy adds energy using meter totals if available, falling back to power integration.
|
|
func (c *Collector) AddEnergy(energyTotal, returnEnergyTotal *float64, power float64) error {
|
|
return c.process(func() {
|
|
switch {
|
|
case energyTotal != nil && returnEnergyTotal != nil:
|
|
c.accu.SetEnergyMeterTotal(*energyTotal)
|
|
c.accu.SetReturnEnergyMeterTotal(*returnEnergyTotal)
|
|
case energyTotal != nil:
|
|
// return energy via power integration (before meter updates clock)
|
|
if power < 0 {
|
|
c.accu.AddPower(power)
|
|
}
|
|
c.accu.SetEnergyMeterTotal(*energyTotal)
|
|
case returnEnergyTotal != nil:
|
|
// energy via power integration (before meter updates clock)
|
|
if power >= 0 {
|
|
c.accu.AddPower(power)
|
|
}
|
|
c.accu.SetReturnEnergyMeterTotal(*returnEnergyTotal)
|
|
default:
|
|
c.accu.AddPower(power)
|
|
}
|
|
})
|
|
}
|