Octopus: support export tariffs (#23742)
This commit is contained in:
parent
b2ca1f4c18
commit
f2e45771ed
5 changed files with 113 additions and 44 deletions
|
|
@ -2,6 +2,7 @@ package tariff
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -16,13 +17,14 @@ import (
|
|||
)
|
||||
|
||||
type Octopus struct {
|
||||
log *util.Logger
|
||||
region string
|
||||
productCode string
|
||||
apikey string
|
||||
accountnumber string
|
||||
paymentMethod string
|
||||
data *util.Monitor[api.Rates]
|
||||
log *util.Logger
|
||||
region string
|
||||
productCode string
|
||||
apikey string
|
||||
accountnumber string
|
||||
paymentMethod string
|
||||
tariffDirection octoGql.TariffDirection
|
||||
data *util.Monitor[api.Rates]
|
||||
}
|
||||
|
||||
var _ api.Tariff = (*Octopus)(nil)
|
||||
|
|
@ -31,14 +33,27 @@ func init() {
|
|||
registry.Add("octopusenergy", NewOctopusFromConfig)
|
||||
}
|
||||
|
||||
// NewOctopusFromConfig creates the tariff provider from the given config map, and runs it.
|
||||
func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) {
|
||||
t, err := buildOctopusFromConfig(other)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return runOrError(t)
|
||||
}
|
||||
|
||||
// buildOctopusFromConfig creates the Tariff provider from the given config map.
|
||||
// Split out to allow for testing.
|
||||
func buildOctopusFromConfig(other map[string]interface{}) (*Octopus, error) {
|
||||
var cc struct {
|
||||
Region string
|
||||
Tariff string // DEPRECATED: use ProductCode
|
||||
ProductCode string
|
||||
DirectDebit bool
|
||||
ApiKey string
|
||||
AccountNumber string
|
||||
Region string
|
||||
Tariff string // DEPRECATED: use ProductCode
|
||||
ProductCode string
|
||||
DirectDebit bool
|
||||
ApiKey string
|
||||
AccountNumber string
|
||||
TariffDirection octoGql.TariffDirection
|
||||
}
|
||||
|
||||
logger := util.NewLogger("octopus")
|
||||
|
|
@ -47,6 +62,19 @@ func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
switch cc.TariffDirection {
|
||||
case "", octoGql.TariffDirectionImport:
|
||||
// default to Import if unset
|
||||
if cc.TariffDirection == "" {
|
||||
cc.TariffDirection = octoGql.TariffDirectionImport
|
||||
}
|
||||
case octoGql.TariffDirectionExport:
|
||||
// OK
|
||||
default:
|
||||
// Do not permit invalid TariffDirections.
|
||||
return nil, fmt.Errorf("invalid tariff direction %q", cc.TariffDirection)
|
||||
}
|
||||
|
||||
// Allow ApiKey to be missing only if Region and Tariff are not.
|
||||
if cc.ApiKey == "" {
|
||||
if cc.Region == "" {
|
||||
|
|
@ -60,12 +88,17 @@ func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) {
|
|||
if cc.ProductCode == "" {
|
||||
return nil, errors.New("missing product code")
|
||||
}
|
||||
if cc.TariffDirection != octoGql.TariffDirectionImport {
|
||||
// Throw a WARN if it appears the user has set the key when it's not necessary to do so
|
||||
logger.WARN.Println("tariffDirection ignored when using product code")
|
||||
}
|
||||
} else {
|
||||
// ApiKey validators
|
||||
if cc.Region != "" || cc.Tariff != "" {
|
||||
return nil, errors.New("cannot use apikey at same time as product code")
|
||||
}
|
||||
if len(cc.ApiKey) != 32 || !strings.HasPrefix(cc.ApiKey, "sk_live_") {
|
||||
// We permit the specific special apiKey "test" as sk_live_ keys are considered Stripe secrets by Github
|
||||
if cc.ApiKey != "test" && (len(cc.ApiKey) != 32 || !strings.HasPrefix(cc.ApiKey, "sk_live_")) {
|
||||
return nil, errors.New("invalid apikey format")
|
||||
}
|
||||
}
|
||||
|
|
@ -74,17 +107,19 @@ func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) {
|
|||
// Not using Direct Debit, filter by non-Direct Debit tariff entries
|
||||
paymentMethod = octoRest.RatePaymentMethodNotDirectDebit
|
||||
}
|
||||
|
||||
t := &Octopus{
|
||||
log: logger,
|
||||
region: cc.Region,
|
||||
productCode: cc.ProductCode,
|
||||
apikey: cc.ApiKey,
|
||||
accountnumber: cc.AccountNumber,
|
||||
paymentMethod: paymentMethod,
|
||||
data: util.NewMonitor[api.Rates](2 * time.Hour),
|
||||
log: logger,
|
||||
region: cc.Region,
|
||||
productCode: cc.ProductCode,
|
||||
apikey: cc.ApiKey,
|
||||
accountnumber: cc.AccountNumber,
|
||||
paymentMethod: paymentMethod,
|
||||
tariffDirection: cc.TariffDirection,
|
||||
data: util.NewMonitor[api.Rates](2 * time.Hour),
|
||||
}
|
||||
|
||||
return runOrError(t)
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *Octopus) run(done chan error) {
|
||||
|
|
@ -101,7 +136,7 @@ func (t *Octopus) run(done chan error) {
|
|||
t.log.ERROR.Println(err)
|
||||
return
|
||||
}
|
||||
tariffCode, err := gqlCli.TariffCode()
|
||||
tariffCode, err := gqlCli.TariffCode(t.tariffDirection)
|
||||
if err != nil {
|
||||
once.Do(func() { done <- err })
|
||||
t.log.ERROR.Println(err)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package graphql
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -131,8 +132,8 @@ func (c *OctopusGraphQLClient) AccountNumber() (accountNumber string, err error)
|
|||
return c.accountNumber, nil
|
||||
}
|
||||
|
||||
// TariffCode queries the Tariff Code of the first IMPORT Electricity Agreement active on the account.
|
||||
func (c *OctopusGraphQLClient) TariffCode() (string, error) {
|
||||
// TariffCode queries the Tariff Code of the first valid Electricity Agreement active on the account that matches the given TariffDirection.
|
||||
func (c *OctopusGraphQLClient) TariffCode(direction TariffDirection) (string, error) {
|
||||
// Update refresh token (if necessary)
|
||||
if err := c.refreshToken(); err != nil {
|
||||
return "", err
|
||||
|
|
@ -156,17 +157,11 @@ func (c *OctopusGraphQLClient) TariffCode() (string, error) {
|
|||
return "", errors.New("no electricity agreements found")
|
||||
}
|
||||
|
||||
// check type
|
||||
// (theoretically) not needed for our uses
|
||||
//switch t := q.Account.ElectricityAgreements[0].Tariff.(type) {
|
||||
//
|
||||
//}
|
||||
|
||||
// Filter out any export tariffs; select the first import tariff.
|
||||
// Filter out any inappropriate tariffs; select the first tariff that aligns with our configuration.
|
||||
var tariffCode string
|
||||
for _, agreement := range q.Account.ElectricityAgreements {
|
||||
if agreement.Tariff.IsExport() {
|
||||
c.log.TRACE.Println("GraphQL: filtering export tariff", agreement.Tariff.TariffCode())
|
||||
if agreement.Tariff.TariffDirection() != direction {
|
||||
c.log.TRACE.Println("GraphQL: filtering tariff with incorrect import/export type:", agreement.Tariff.TariffCode())
|
||||
continue
|
||||
}
|
||||
tariffCode = agreement.Tariff.TariffCode()
|
||||
|
|
@ -174,7 +169,7 @@ func (c *OctopusGraphQLClient) TariffCode() (string, error) {
|
|||
}
|
||||
|
||||
if tariffCode == "" {
|
||||
return "", errors.New("no import electricity agreement found")
|
||||
return "", fmt.Errorf("no electricity agreement for type %s", direction)
|
||||
}
|
||||
|
||||
c.log.TRACE.Println("GraphQL: tariff code found:", tariffCode)
|
||||
|
|
|
|||
|
|
@ -38,9 +38,22 @@ func (d *tariffData) TariffCode() string {
|
|||
return d.standardTariff.TariffCode
|
||||
}
|
||||
|
||||
// IsExport is a shortcut function for determining whether the given tariff is for export, regardless of tariff type.
|
||||
func (d *tariffData) IsExport() bool {
|
||||
return d.standardTariff.IsExport
|
||||
// TariffDirection defines which direction of energy flow is being denoted by the given tariff.
|
||||
type TariffDirection string
|
||||
|
||||
const (
|
||||
// TariffDirectionImport is for energy flow INTO the meter FROM the grid (to the property)
|
||||
TariffDirectionImport TariffDirection = "import"
|
||||
|
||||
// TariffDirectionExport is for energy flow OUT OF the meter FROM the property (to the grid)
|
||||
TariffDirectionExport TariffDirection = "export"
|
||||
)
|
||||
|
||||
func (d *tariffData) TariffDirection() TariffDirection {
|
||||
if d.standardTariff.IsExport {
|
||||
return TariffDirectionExport
|
||||
}
|
||||
return TariffDirectionImport
|
||||
}
|
||||
|
||||
type tariffType struct {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ func TestOctopusConfigParse(t *testing.T) {
|
|||
"directDebit": "True",
|
||||
}
|
||||
|
||||
_, err := NewOctopusFromConfig(validTariffConfig)
|
||||
_, err := buildOctopusFromConfig(validTariffConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
validProductCodeConfig := map[string]interface{}{
|
||||
|
|
@ -26,14 +26,29 @@ func TestOctopusConfigParse(t *testing.T) {
|
|||
"directDebit": "False",
|
||||
}
|
||||
|
||||
_, err = NewOctopusFromConfig(validProductCodeConfig)
|
||||
_, err = buildOctopusFromConfig(validProductCodeConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidApiAndProductCodeConfig := map[string]interface{}{
|
||||
"region": "H",
|
||||
"productcode": "GO-22-03-29",
|
||||
"apikey": "nope",
|
||||
"region": "H",
|
||||
"productcode": "GO-22-03-29",
|
||||
"tariffDirection": "import",
|
||||
"apikey": "invalid_key",
|
||||
}
|
||||
_, err = NewOctopusFromConfig(invalidApiAndProductCodeConfig)
|
||||
_, err = buildOctopusFromConfig(invalidApiAndProductCodeConfig)
|
||||
require.Error(t, err)
|
||||
|
||||
invalidTariffDirectionConfig := map[string]interface{}{
|
||||
"tariffDirection": "invalid",
|
||||
"apikey": "test",
|
||||
}
|
||||
_, err = buildOctopusFromConfig(invalidTariffDirectionConfig)
|
||||
require.Errorf(t, err, "invalid tariff type")
|
||||
|
||||
validApiExportConfig := map[string]interface{}{
|
||||
"tariffDirection": "export",
|
||||
"apikey": "test",
|
||||
}
|
||||
_, err = buildOctopusFromConfig(validApiExportConfig)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,18 @@ params:
|
|||
en: "Only required if you have multiple accounts."
|
||||
de: "Nur erforderlich, wenn mehrere Konten vorhanden sind."
|
||||
example: "X-XXXXXXXX"
|
||||
- name: tariffDirection
|
||||
type: choice
|
||||
choice: ["import", "export"]
|
||||
required: false
|
||||
default: "import"
|
||||
description:
|
||||
generic: The tariff flow direction to query from Octopus.
|
||||
help:
|
||||
generic: "Set to 'export' when using feedin:"
|
||||
|
||||
render: |
|
||||
type: octopusenergy
|
||||
apikey: {{ .apiKey }}
|
||||
accountNumber: {{ .accountNumber }}
|
||||
tariffDirection: {{ .tariffDirection }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue