150 lines
3.8 KiB
Go
150 lines
3.8 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" // additional meter (ext, monitoring only)
|
|
Consumer = "consumer" // consumer meter (consumers list or 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 only for directions without an energy meter. A direction that has
|
|
// reported a total before keeps using meter deltas even if a single read fails,
|
|
// so a transient failure is recovered via the next delta and not double-counted.
|
|
func (c *Collector) AddEnergy(energyTotal, returnEnergyTotal *float64, power float64) error {
|
|
return c.process(func() {
|
|
// a direction that ever reported a total is metered, so a nil read is a
|
|
// transient failure rather than a power-only meter
|
|
hasEnergyMeter := energyTotal != nil || c.accu.energyMeter != nil
|
|
hasReturnMeter := returnEnergyTotal != nil || c.accu.returnEnergyMeter != nil
|
|
|
|
// integrate power for the unmetered direction first, since applying a
|
|
// meter total advances the accumulator clock
|
|
if power >= 0 {
|
|
if !hasEnergyMeter {
|
|
c.accu.AddPower(power)
|
|
}
|
|
} else if !hasReturnMeter {
|
|
c.accu.AddPower(power)
|
|
}
|
|
|
|
if energyTotal != nil {
|
|
c.accu.SetEnergyMeterTotal(*energyTotal)
|
|
}
|
|
if returnEnergyTotal != nil {
|
|
c.accu.SetReturnEnergyMeterTotal(*returnEnergyTotal)
|
|
}
|
|
})
|
|
}
|