-
-
Notifications
You must be signed in to change notification settings - Fork 641
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Octopusenergy: support API keys for tariff data lookup (#13637)
- Loading branch information
1 parent
090b0a7
commit 33aa884
Showing
7 changed files
with
373 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package graphql | ||
|
||
// krakenTokenAuthentication is a representation of a GraphQL query for obtaining a Kraken API token. | ||
type krakenTokenAuthentication struct { | ||
ObtainKrakenToken struct { | ||
Token string | ||
} `graphql:"obtainKrakenToken(input: {APIKey: $apiKey})"` | ||
} | ||
|
||
// krakenAccountLookup is a representation of a GraphQL query for obtaining the Account Number associated with the | ||
// credentials used to authorize the request. | ||
type krakenAccountLookup struct { | ||
Viewer struct { | ||
Accounts []struct { | ||
Number string | ||
} | ||
} | ||
} | ||
|
||
type tariffData struct { | ||
// yukky but the best way I can think of to handle this | ||
// access via any relevant tariff data entry (i.e. standardTariff) | ||
standardTariff `graphql:"... on StandardTariff"` | ||
dayNightTariff `graphql:"... on DayNightTariff"` | ||
threeRateTariff `graphql:"... on ThreeRateTariff"` | ||
halfHourlyTariff `graphql:"... on HalfHourlyTariff"` | ||
prepayTariff `graphql:"... on PrepayTariff"` | ||
} | ||
|
||
// TariffCode is a shortcut function to obtaining the Tariff Code of the given tariff, regardless of tariff type. | ||
// Developer Note: GraphQL query returns the same element keys regardless of type, | ||
// so it should always be decoded as standardTariff at least. | ||
// We are unlikely to use the other Tariff types for data access (?). | ||
func (d *tariffData) TariffCode() string { | ||
return d.standardTariff.TariffCode | ||
} | ||
|
||
type tariffType struct { | ||
Id string | ||
DisplayName string | ||
FullName string | ||
ProductCode string | ||
StandingCharge float32 | ||
PreVatStandingCharge float32 | ||
} | ||
|
||
type tariffTypeWithTariffCode struct { | ||
tariffType | ||
TariffCode string | ||
} | ||
|
||
type standardTariff struct { | ||
tariffTypeWithTariffCode | ||
} | ||
type dayNightTariff struct { | ||
tariffTypeWithTariffCode | ||
} | ||
type threeRateTariff struct { | ||
tariffTypeWithTariffCode | ||
} | ||
type halfHourlyTariff struct { | ||
tariffTypeWithTariffCode | ||
} | ||
type prepayTariff struct { | ||
tariffTypeWithTariffCode | ||
} | ||
|
||
type krakenAccountElectricityAgreements struct { | ||
Account struct { | ||
ElectricityAgreements []struct { | ||
Id int | ||
Tariff tariffData | ||
MeterPoint struct { | ||
// Mpan is the serial number of the meter that this ElectricityAgreement is bound to. | ||
Mpan string | ||
} | ||
} `graphql:"electricityAgreements(active: true)"` | ||
} `graphql:"account(accountNumber: $accountNumber)"` | ||
} |
Oops, something went wrong.