package tariff import ( "errors" "fmt" "slices" "sync" "time" "github.com/cenkalti/backoff/v4" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/tariff/corrently" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" "golang.org/x/oauth2" ) type GrünStromIndex struct { *request.Helper log *util.Logger zip string data *util.Monitor[api.Rates] } var _ api.Tariff = (*GrünStromIndex)(nil) func init() { registry.Add("grünstromindex", NewGrünStromIndexFromConfig) } func NewGrünStromIndexFromConfig(other map[string]any) (api.Tariff, error) { var cc struct { Zip string Token string } if err := util.DecodeOther(other, &cc); err != nil { return nil, err } log := util.NewLogger("gsi").Redact(cc.Zip, cc.Token) t := &GrünStromIndex{ log: log, zip: cc.Zip, Helper: request.NewHelper(log), data: util.NewMonitor[api.Rates](2 * time.Hour), } t.Client.Transport = &oauth2.Transport{ Base: t.Client.Transport, Source: corrently.TokenSource(log, &oauth2.Token{AccessToken: cc.Token}), } return runOrError(t) } func (t *GrünStromIndex) run(done chan error) { var once sync.Once uri := fmt.Sprintf("https://api.corrently.io/v2.0/gsi/prediction?zip=%s", t.zip) for tick := time.Tick(time.Hour); ; <-tick { var res corrently.Forecast err := backoff.Retry(func() error { return backoffPermanentError(t.GetJSON(uri, &res)) }, bo()) if err == nil && res.Err { if s, ok := res.Message.(string); ok { err = errors.New(s) } else { err = api.ErrNotAvailable } } var data api.Rates if err == nil { if data = gsiRates(res); len(data) == 0 { err = api.ErrNotAvailable } } if err != nil { if reportError(&once, done, err) { return } t.log.ERROR.Println(err) continue } mergeRates(t.data, data) once.Do(func() { close(done) }) } } // gsiRates maps the forecast to rates. co2_g_standard is null since mid-2026, // fall back to co2_g_oekostrom and skip slots without either value. func gsiRates(res corrently.Forecast) api.Rates { data := make(api.Rates, 0, len(res.Forecast)) for _, r := range res.Forecast { co2 := r.Co2GStandard if co2 == nil { co2 = r.Co2GOekostrom } if co2 == nil { continue } data = append(data, api.Rate{ Start: time.UnixMilli(r.Timeframe.Start).Local(), End: time.UnixMilli(r.Timeframe.End).Local(), Value: float64(*co2), }) } return data } // Rates implements the api.Tariff interface func (t *GrünStromIndex) Rates() (api.Rates, error) { var res api.Rates err := t.data.GetFunc(func(val api.Rates) { res = slices.Clone(val) }) return res, err } // Type implements the api.Tariff interface func (t *GrünStromIndex) Type() api.TariffType { return api.TariffTypeCo2 }