Skip to content

Commit

Permalink
Grünstromindex: request token on demand (#17928)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Dec 28, 2024
1 parent f470ccd commit 752221e
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 11 deletions.
40 changes: 40 additions & 0 deletions tariff/corrently/tokensource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package corrently

import (
"net/http"
"time"

"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/oauth"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)

type tokenSource struct {
log *util.Logger
}

func TokenSource(log *util.Logger, token *oauth2.Token) oauth2.TokenSource {
return oauth.RefreshTokenSource(token, &tokenSource{log})
}

func (ts *tokenSource) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
// "Content-Type: application/json" \
// --request POST \
// https://console.corrently.io/v2.0/auth/requestToken

var res struct {
Token string `json:"token"`
Expires int64 `json:"expires"`
}

req, _ := request.New(http.MethodPost, "https://console.corrently.io/v2.0/auth/requestToken", nil, request.JSONEncoding)
err := request.NewHelper(ts.log).DoJSON(req, &res)

token := &oauth2.Token{
AccessToken: res.Token,
Expiry: time.UnixMilli(res.Expires),
}

return token, err
}
38 changes: 38 additions & 0 deletions tariff/corrently/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package corrently

type Forecast struct {
Support string `json:"support"`
License string `json:"license"`
Info string `json:"info"`
Documentation string `json:"documentation"`
Commercial string `json:"commercial"`
Signee string `json:"signee"`
Forecast []struct {
Epochtime int `json:"epochtime"`
Eevalue int `json:"eevalue"`
Ewind int `json:"ewind"`
Esolar int `json:"esolar"`
Ensolar int `json:"ensolar"`
Enwind int `json:"enwind"`
Sci int `json:"sci"`
Gsi float64 `json:"gsi"`
TimeStamp int64 `json:"timeStamp"`
Energyprice string `json:"energyprice"`
Co2GStandard int `json:"co2_g_standard"`
Co2GOekostrom int `json:"co2_g_oekostrom"`
Timeframe struct {
Start int64 `json:"start"`
End int64 `json:"end"`
} `json:"timeframe"`
Iat int64 `json:"iat"`
Zip string `json:"zip"`
Signature string `json:"signature"`
} `json:"forecast"`
Location struct {
Zip string `json:"zip"`
City string `json:"city"`
Signature string `json:"signature"`
} `json:"location"`
Err bool
Message any
}
114 changes: 114 additions & 0 deletions tariff/gruenstromindex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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]interface{}) (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}),
}

done := make(chan error)
go t.run(done)
err := <-done

return t, err
}

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
}
}

if err != nil {
once.Do(func() { done <- err })

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

data := make(api.Rates, 0, len(res.Forecast))
for _, r := range res.Forecast {
data = append(data, api.Rate{
Price: float64(r.Co2GStandard),
Start: time.UnixMilli(r.Timeframe.Start).Local(),
End: time.UnixMilli(r.Timeframe.End).Local(),
})
}

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

// 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
}
14 changes: 3 additions & 11 deletions templates/definition/tariff/gruenstromindex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ params:
de: "Token für den Zugriff auf die API von https://console.corrently.io/"
en: "Token for accessing the API from https://console.corrently.io"
render: |
type: custom
tariff: co2
forecast:
source: http
uri: https://api.corrently.io/v2.0/gsi/prediction?zip={{ .zip }}&token={{ .token }}
jq: |
[ .forecast.[] | {
start: .timeframe.start / 1000 | todate,
end: .timeframe.end / 1000 | todate,
price: .co2_g_standard
} ] | tostring
type: grünstromindex
zip: {{ .zip }}
token: {{ .token }}

0 comments on commit 752221e

Please sign in to comment.