Ctrl K
ComunidadGuías…

tariff/localvolts.go

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

Archivo original ↗Preguntar a KiBot
package tariff

import (
	"errors"
	"fmt"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/cenkalti/backoff/v4"
	"github.com/evcc-io/evcc/api"
	"github.com/evcc-io/evcc/util"
	"github.com/evcc-io/evcc/util/request"
	"github.com/evcc-io/evcc/util/transport"
)

// https://localvolts.com/wp-content/uploads/2026/10/LvAPI_20261002.pdf
const localVoltsURI = "https://api.localvolts.com/v2/customer/interval?NMI=%s&from=current&to=1day"

type localVoltsInterval struct {
	Circuit          string    `json:"circuit"`
	IntervalEnd      time.Time `json:"intervalEnd"`
	IntervalDuration int       `json:"intervalDuration,string"` // minutes, sent as string despite the docs
	FlexUp           float64   `json:"flexUp"`                  // c/kWh
}

type LocalVolts struct {
	*embed
	*request.Helper
	log     *util.Logger
	uri     string
	channel string
	data    *util.Monitor[api.Rates]
}

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

func init() {
	registry.Add("localvolts", NewLocalVoltsFromConfig)
}

func NewLocalVoltsFromConfig(other map[string]any) (api.Tariff, error) {
	var cc struct {
		embed   `mapstructure:",squash"`
		ApiKey  string
		Partner string
		Nmi     string
		Channel string
	}

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

	if cc.ApiKey == "" || cc.Partner == "" {
		return nil, api.ErrMissingCredentials
	}

	if cc.Nmi == "" {
		return nil, errors.New("missing nmi")
	}

	if cc.Channel == "" {
		return nil, errors.New("missing channel")
	}

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

	log := util.NewLogger("localvolts").Redact(cc.ApiKey)

	t := &LocalVolts{
		embed:   &cc.embed,
		log:     log,
		Helper:  request.NewHelper(log),
		uri:     fmt.Sprintf(localVoltsURI, cc.Nmi),
		channel: cc.Channel,
		data:    util.NewMonitor[api.Rates](2 * time.Hour),
	}

	t.Client.Transport = &transport.Decorator{
		Base: t.Client.Transport,
		Decorator: transport.DecorateHeaders(map[string]string{
			"Authorization": "apikey " + cc.ApiKey,
			"partner":       cc.Partner,
		}),
	}

	return runOrError(t)
}

func (t *LocalVolts) run(done chan error) {
	var once sync.Once

	for tick := time.Tick(time.Minute); ; <-tick {
		var res []localVoltsInterval

		if err := backoff.Retry(func() error {
			return backoffPermanentError(t.GetJSON(t.uri, &res))
		}, bo()); err != nil {
			if reportError(&once, done, err) {
				return
			}

			t.log.ERROR.Println(err)
			continue
		}

		data := t.rates(res)
		if len(data) == 0 {
			err := fmt.Errorf("no data for channel %s", t.channel)
			if reportError(&once, done, err) {
				return
			}

			t.log.ERROR.Println(err)
			continue
		}

		mergeRates(t.data, data)
		once.Do(func() { close(done) })
	}
}

// rates averages the 5-minute NEM intervals of the configured circuit into 15-minute slots.
// flexUp is the marginal price per kWh: positive means cost on Import and revenue on Export,
// which matches evcc's sign convention for grid and feed-in tariffs.
func (t *LocalVolts) rates(res []localVoltsInterval) api.Rates {
	type bucket struct {
		sum float64
		n   int
	}
	buckets := make(map[time.Time]*bucket)

	for _, r := range res {
		if !strings.EqualFold(r.Circuit, t.channel) {
			continue
		}

		start := r.IntervalEnd.Add(-time.Duration(r.IntervalDuration) * time.Minute).Local()
		slot := start.Truncate(SlotDuration)

		b, ok := buckets[slot]
		if !ok {
			b = &bucket{}
			buckets[slot] = b
		}

		b.sum += t.totalPrice(r.FlexUp/1e2, start)
		b.n++
	}

	data := make(api.Rates, 0, len(buckets))
	for slot, b := range buckets {
		data = append(data, api.Rate{
			Start: slot,
			End:   slot.Add(SlotDuration),
			Value: b.sum / float64(b.n),
		})
	}
	data.Sort()

	return data
}

// Rates implements the api.Tariff interface
func (t *LocalVolts) Rates() (api.Rates, error) {
	var res api.Rates
	err := t.data.GetFunc(func(val api.Rates) {
		res = slices.Clone(val)
	})
	return res, err
}

func (t *LocalVolts) Unit() string {
	return "AUD"
}

// Type implements the api.Tariff interface
func (t *LocalVolts) Type() api.TariffType {
	return api.TariffTypePriceForecast
}