package tariff import ( "sync" "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" ) const SlotDuration = 15 * time.Minute type SlotWrapper struct { api.Tariff once sync.Once } // shortSlot returns the first slot shorter than SlotDuration. The leading slot // is skipped since it may legitimately be truncated to the current period. func shortSlot(rr api.Rates) (api.Rate, bool) { for _, r := range rr[min(1, len(rr)):] { if r.End.Sub(r.Start) < SlotDuration { return r, true } } return api.Rate{}, false } // Rates converts arbitrary slot lengths (multiples of SlotDuration) to 15m slots. // Price sub-slots are constant, solar sub-slots interpolated towards the next slot. func (t *SlotWrapper) Rates() (api.Rates, error) { rates, err := t.Tariff.Rates() if err != nil { return nil, err } if r, ok := shortSlot(rates); ok { t.once.Do(func() { util.NewLogger("tariff").WARN.Printf("slot duration %v shorter than %v, results will be incorrect", r.End.Sub(r.Start), SlotDuration) }) } var res api.Rates if len(rates) > 0 { // assume all slots of equal length res = make(api.Rates, 0, len(rates)*max(int(rates[0].End.Sub(rates[0].Start)/SlotDuration), 1)) } now := time.Now().Truncate(SlotDuration) for i, r := range rates { if !r.End.After(now) { // only keep slots >= now continue } numSlots := max(int(r.End.Sub(r.Start)/SlotDuration), 1) vals := make([]float64, numSlots) for j := range vals { vals[j] = r.Value } if t.Type() == api.TariffTypeSolar { shapeSolar(rates, i, vals) } for j := range numSlots { start := r.Start.Add(time.Duration(j) * SlotDuration) res = append(res, api.Rate{ Start: start, End: start.Add(SlotDuration), Value: vals[j], }) } } return res, nil } // shapeSolar samples the source curve at the sub-slot starts. A solar value is the // power at its Start (see core.solarEnergy), so splitting a slot must interpolate // towards the successor rather than redistribute the value across the sub-slots - // only then does the split leave the integrated energy untouched. Interpolation runs // over the distance between the two starts, which is the slot length only for a gapless // series. The trailing slot has no successor and stays flat. func shapeSolar(rates api.Rates, i int, vals []float64) { cur := rates[i].Value for j := range vals { vals[j] = cur } if i+1 >= len(rates) { return } next := rates[i+1] span := next.Start.Sub(rates[i].Start) if span <= 0 { return } for j := range vals { // beyond the successor's start the curve is the successor's business d := min(time.Duration(j)*SlotDuration, span) vals[j] = cur + (next.Value-cur)*float64(d)/float64(span) } }