evcc-io/tariff/octopus/graphql/api.go

147 lines
4 KiB
Go

package graphql
import (
"context"
"errors"
"net/http"
"sync"
"time"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/hasura/go-graphql-client"
)
// BaseURI is Octopus Energy's core API root.
const BaseURI = "https://api.octopus.energy"
// URI is the GraphQL query endpoint for Octopus Energy.
const URI = BaseURI + "/v1/graphql/"
// OctopusGraphQLClient provides an interface for communicating with Octopus Energy's Kraken platform.
type OctopusGraphQLClient struct {
*graphql.Client
// apikey is the Octopus Energy API key (provided by user)
apikey string
// token is the GraphQL token used for communication with kraken (we get this ourselves with the apikey)
token *string
// tokenExpiration tracks the expiry of the acquired token. A new Token should be obtained if this time is passed.
tokenExpiration time.Time
// tokenMtx should be held when requesting a new token.
tokenMtx sync.Mutex
// accountNumber is the Octopus Energy account number associated with the given API key (queried ourselves via GraphQL)
accountNumber string
}
// NewClient returns a new, unauthenticated instance of OctopusGraphQLClient.
func NewClient(log *util.Logger, apikey string) (*OctopusGraphQLClient, error) {
cli := request.NewClient(log)
gq := &OctopusGraphQLClient{
Client: graphql.NewClient(URI, cli),
apikey: apikey,
}
if err := gq.refreshToken(); err != nil {
return nil, err
}
// Future requests must have the appropriate Authorization header set.
gq.Client = gq.Client.WithRequestModifier(func(r *http.Request) {
gq.tokenMtx.Lock()
defer gq.tokenMtx.Unlock()
r.Header.Add("Authorization", *gq.token)
})
return gq, nil
}
// refreshToken updates the GraphQL token from the set apikey.
// Basic caching is provided - it will not update the token if it hasn't expired yet.
func (c *OctopusGraphQLClient) refreshToken() error {
// take a lock against the token mutex for the refresh
c.tokenMtx.Lock()
defer c.tokenMtx.Unlock()
if time.Until(c.tokenExpiration) > 5*time.Minute {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var q krakenTokenAuthentication
if err := c.Client.Mutate(ctx, &q, map[string]interface{}{"apiKey": c.apikey}); err != nil {
return err
}
c.token = &q.ObtainKrakenToken.Token
c.tokenExpiration = time.Now().Add(time.Hour)
return nil
}
// AccountNumber queries the Account Number assigned to the associated API key.
// Caching is provided.
func (c *OctopusGraphQLClient) AccountNumber() (string, error) {
// Check cache
if c.accountNumber != "" {
return c.accountNumber, nil
}
// Update refresh token (if necessary)
if err := c.refreshToken(); err != nil {
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var q krakenAccountLookup
if err := c.Client.Query(ctx, &q, nil); err != nil {
return "", err
}
if len(q.Viewer.Accounts) == 0 {
return "", errors.New("no account associated with given octopus api key")
}
if len(q.Viewer.Accounts) > 1 {
return "", errors.New("more than one octopus account on this api key not supported")
}
c.accountNumber = q.Viewer.Accounts[0].Number
return c.accountNumber, nil
}
// TariffCode queries the Tariff Code of the first Electricity Agreement active on the account.
func (c *OctopusGraphQLClient) TariffCode() (string, error) {
// Update refresh token (if necessary)
if err := c.refreshToken(); err != nil {
return "", err
}
// Get Account Number
acc, err := c.AccountNumber()
if err != nil {
return "", nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var q krakenAccountElectricityAgreements
if err := c.Client.Query(ctx, &q, map[string]interface{}{"accountNumber": acc}); err != nil {
return "", err
}
if len(q.Account.ElectricityAgreements) == 0 {
return "", errors.New("no electricity agreements found")
}
// check type
//switch t := q.Account.ElectricityAgreements[0].Tariff.(type) {
//
//}
return q.Account.ElectricityAgreements[0].Tariff.TariffCode(), nil
}