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