Ctrl K
ComunidadGuías…

tariff/slots_test.go

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

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"testing"
	"time"

	"github.com/evcc-io/evcc/api"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type testTariff struct {
	rates api.Rates
	typ   api.TariffType
}

func (t *testTariff) Rates() (api.Rates, error) {
	return t.rates, nil
}
func (t *testTariff) Type() api.TariffType {
	return t.typ
}

// makeRates creates n consecutive rates starting at 'start', each with the given duration
// Values start at startVal and increase by 1 for each subsequent rate
func makeRates(start time.Time, duration time.Duration, n int, startVal float64) api.Rates {
	var rates api.Rates
	for i := range n {
		s := start.Add(time.Duration(i) * duration)
		rates = append(rates, api.Rate{
			Start: s,
			End:   s.Add(duration),
			Value: startVal + float64(i),
		})
	}
	return rates
}

// TestBasicSlotConversionCounts ensures that different source durations are split into the expected number of 15-minute slots
func TestBasicSlotConversionCounts(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	cases := []struct {
		dur      time.Duration
		expected int
	}{
		{15 * time.Minute, 1},
		{30 * time.Minute, 2},
		{1 * time.Hour, 4},
		{2 * time.Hour, 8},
	}

	for _, tc := range cases {
		// Create a single rate of length tc.dur starting at "now"
		rates := makeRates(now, tc.dur, 1, 5.0)
		w := &SlotWrapper{Tariff: &testTariff{
			rates: rates,
			typ:   api.TariffTypePriceStatic,
		}}

		out, err := w.Rates()
		require.NoError(t, err)

		// Check the number of produced 15-minute slots
		assert.Len(t, out, tc.expected, "duration %v", tc.dur)

		// Additional lightweight checks:
		// - first slot should begin at the original rate start
		// - every produced slot must have the configured SlotDuration length
		if len(out) > 0 {
			assert.Equal(t, rates[0].Start, out[0].Start)
			for _, r := range out {
				assert.Equal(t, SlotDuration, r.End.Sub(r.Start))
			}
		}
	}
}

// TestMixedSlots verifies a mix of a 15-minute rate followed by a 1-hour rate
// For price tariffs subslots from the hour should keep the same constant price
func TestMixedSlots(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	// first: a single 15-minute rate
	r0 := api.Rate{
		Start: now,
		End:   now.Add(15 * time.Minute),
		Value: 1.0,
	}

	// second: an hour that follows immediately
	r1 := api.Rate{
		Start: r0.End,
		End:   r0.End.Add(1 * time.Hour),
		Value: 3.0,
	}

	w := &SlotWrapper{Tariff: &testTariff{
		rates: api.Rates{r0, r1},
		typ:   api.TariffTypePriceStatic,
	}}

	out, err := w.Rates()
	require.NoError(t, err)

	// expected: one 15m slot with value 1.0, then four 15m slots with value 3.0
	expected := api.Rates{api.Rate{
		Start: r0.Start,
		End:   r0.End,
		Value: 1.0,
	}}

	for i := range 4 {
		expected = append(expected, api.Rate{
			Start: r1.Start.Add(time.Duration(i) * SlotDuration),
			End:   r1.Start.Add(time.Duration(i+1) * SlotDuration),
			Value: 3.0,
		})
	}

	assert.Equal(t, expected, out)
}

func TestDropOldRates(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	// old rate that should be removed by the wrapper (ends before 'now')
	old := api.Rate{
		Start: now.Add(-1 * time.Hour),
		End:   now,
		Value: 0.5,
	}

	w := &SlotWrapper{Tariff: &testTariff{
		rates: api.Rates{old},
		typ:   api.TariffTypeSolar,
	}}

	res, err := w.Rates()
	require.NoError(t, err)
	require.Len(t, res, 0)
}

// assertSourceValues verifies that each source slot start keeps its original value
func assertSourceValues(t *testing.T, rr, res api.Rates) {
	t.Helper()

	n := len(res) / len(rr)
	for i, r := range rr {
		assert.InDelta(t, r.Value, res[i*n].Value, 1e-9, "rate %d", i)
	}
}

// TestSolarInterpolation verifies that solar sub-slots ramp towards the
// following slot while keeping the source value at the source slot start
func TestSolarInterpolation(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	// two consecutive hourly solar rates: 0.0 in the first hour, 4.0 in the next
	r0 := api.Rate{
		Start: now,
		End:   now.Add(time.Hour),
		Value: 0.0,
	}
	r1 := api.Rate{
		Start: r0.End,
		End:   r0.End.Add(time.Hour),
		Value: 4.0,
	}

	w := &SlotWrapper{Tariff: &testTariff{
		rates: api.Rates{r0, r1},
		typ:   api.TariffTypeSolar,
	}}

	res, err := w.Rates()
	require.NoError(t, err)
	require.Len(t, res, 8)

	for i, r := range res {
		assert.Equal(t, now.Add(time.Duration(i)*SlotDuration), r.Start, "slot %d", i)
	}

	// ramping up towards the next hour, flat towards the missing successor
	for i, expected := range []float64{0, 1, 2, 3, 4, 4, 4, 4} {
		assert.InDelta(t, expected, res[i].Value, 1e-9, "slot %d", i)
	}

	assertSourceValues(t, api.Rates{r0, r1}, res)
}

// TestSolarInterpolationInterior verifies an interior slot with both neighbours differing
func TestSolarInterpolationInterior(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	rr := make(api.Rates, 3)
	for i, v := range []float64{0, 4, 8} {
		start := now.Add(time.Duration(i) * time.Hour)
		rr[i] = api.Rate{Start: start, End: start.Add(time.Hour), Value: v}
	}

	w := &SlotWrapper{Tariff: &testTariff{rates: rr, typ: api.TariffTypeSolar}}

	res, err := w.Rates()
	require.NoError(t, err)
	require.Len(t, res, 12)

	// interior slot ramps linearly from its own value to the next one
	for i, expected := range []float64{4, 5, 6, 7} {
		assert.InDelta(t, expected, res[4+i].Value, 1e-9, "slot %d", i)
	}

	assertSourceValues(t, rr, res)
}

// TestSolarInterpolationGap verifies that the ramp spans the distance between the
// two slot starts, not the slot length, when the series has a gap
func TestSolarInterpolationGap(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	// one hour, then a one hour gap before the successor
	rr := api.Rates{
		{Start: now, End: now.Add(time.Hour), Value: 0},
		{Start: now.Add(2 * time.Hour), End: now.Add(3 * time.Hour), Value: 8},
	}

	w := &SlotWrapper{Tariff: &testTariff{rates: rr, typ: api.TariffTypeSolar}}

	res, err := w.Rates()
	require.NoError(t, err)
	require.Len(t, res, 8)

	// ramp reaches the successor after two hours, not after one
	for i, expected := range []float64{0, 1, 2, 3} {
		assert.InDelta(t, expected, res[i].Value, 1e-9, "slot %d", i)
	}
}

// TestSolarNegativeSlot verifies that a non-positive slot ramps like any other
func TestSolarNegativeSlot(t *testing.T) {
	now := time.Now().Truncate(SlotDuration)

	rr := api.Rates{
		{Start: now, End: now.Add(time.Hour), Value: -1},
		{Start: now.Add(time.Hour), End: now.Add(2 * time.Hour), Value: 4},
	}

	w := &SlotWrapper{Tariff: &testTariff{rates: rr, typ: api.TariffTypeSolar}}

	res, err := w.Rates()
	require.NoError(t, err)
	require.Len(t, res, 8)

	for i, expected := range []float64{-1, 0.25, 1.5, 2.75} {
		assert.InDelta(t, expected, res[i].Value, 1e-9, "slot %d", i)
	}
}