Ctrl K
ComunidadGuías…

tariff/fixed.go

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

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"fmt"
	"slices"
	"sort"
	"time"

	"github.com/benbjohnson/clock"
	"github.com/evcc-io/evcc/api"
	"github.com/evcc-io/evcc/tariff/fixed"
	"github.com/evcc-io/evcc/util"
	"github.com/jinzhu/now"
)

type Fixed struct {
	*embed
	clock   clock.Clock
	zones   fixed.Zones
	dynamic bool
}

var _ api.Tariff = (*Fixed)(nil)

func init() {
	registry.Add("fixed", NewFixedFromConfig)
}

func NewFixedFromConfig(other map[string]any) (api.Tariff, error) {
	var cc struct {
		embed `mapstructure:",squash"`
		Price float64
		Zones []fixed.ZoneSpec
	}

	if err := util.DecodeOther(other, &cc); err != nil {
		return nil, err
	}

	if err := cc.embed.init(); err != nil {
		return nil, err
	}

	zones, err := fixed.ParseZones(cc.Zones)
	if err != nil {
		return nil, err
	}

	t := &Fixed{
		embed:   &cc.embed,
		clock:   clock.New(),
		dynamic: len(cc.Zones) >= 1 || len(cc.ChargesZones_) >= 1,
		zones:   zones,
	}

	sort.Sort(t.zones)

	// prepend catch-all zone
	t.zones = append([]fixed.Zone{
		{Price: cc.Price}, // full week is implicit
	}, t.zones...)

	return t, nil
}

// wallClock returns the local time the given minutes after midnight, independent of DST offset changes
func wallClock(dayStart time.Time, minutes int) time.Time {
	ts := time.Date(dayStart.Year(), dayStart.Month(), dayStart.Day(), 0, minutes, 0, 0, dayStart.Location())

	// dst spring forward: map wall times inside the skipped hour to its end, keeping markers ordered
	if 60*ts.Hour()+ts.Minute() != minutes {
		ts, _ = ts.ZoneBounds()
	}

	return ts
}

// Rates implements the api.Tariff interface
func (t *Fixed) Rates() (api.Rates, error) {
	var res api.Rates

	start := now.With(t.clock.Now()).BeginningOfDay()
	for i := range 7 {
		dayStart := start.AddDate(0, 0, i)
		dow := fixed.Day((int(start.Weekday()) + i) % 7)
		month := fixed.Month(dayStart.Month() - 1)

		zones := t.zones.ForDayAndMonth(dow, month)
		if len(zones) == 0 {
			return nil, fmt.Errorf("no zones for weekday %d", dow)
		}

		// include chargesZones boundaries so rate changes there are not swallowed by the coarser price zone markers
		markers := append(slices.Clone(zones), t.chargesZones.ForDayAndMonth(dow, month)...).TimeTableMarkers()

		for i, m := range markers {
			ts := wallClock(dayStart, m.Minutes())

			var zone *fixed.Zone
			for j := len(zones) - 1; j >= 0; j-- {
				if zones[j].Hours.Contains(m) {
					zone = &zones[j]
					break
				}
			}

			if zone == nil {
				return nil, fmt.Errorf("could not find zone for %02d:%02d", m.Hour, m.Min)
			}

			// end rate at end of day or next marker
			end := dayStart.AddDate(0, 0, 1)
			if i+1 < len(markers) {
				end = wallClock(dayStart, markers[i+1].Minutes())
			}

			// dst spring forward: last marker coincides with end of day
			if !end.After(ts) {
				continue
			}

			rate := api.Rate{
				Start: ts,
				End:   end,
				Value: t.totalPrice(zone.Price, ts),
			}

			res = append(res, rate)
		}
	}

	return res, nil
}

// Type implements the api.Tariff interface
func (t *Fixed) Type() api.TariffType {
	if t.dynamic {
		return api.TariffTypePriceForecast
	}
	return api.TariffTypePriceStatic
}