Ctrl K
ComunidadGuías…

tariff/slots.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
	}
}