Add Octopus Energy Italy tariff (#31505)
This commit is contained in:
parent
8a771d5fb3
commit
de976b7aa2
13 changed files with 360 additions and 67 deletions
|
|
@ -10,17 +10,17 @@ import (
|
|||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
octoDeGql "github.com/evcc-io/evcc/tariff/octopusde/graphql"
|
||||
krakengql "github.com/evcc-io/evcc/tariff/octopuskraken/graphql"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/jinzhu/now"
|
||||
)
|
||||
|
||||
// ErrAuthFailed re-exports the GraphQL auth-failure sentinel for use in tests.
|
||||
var ErrAuthFailed = octoDeGql.ErrAuthFailed
|
||||
var ErrAuthFailed = krakengql.ErrAuthFailed
|
||||
|
||||
type OctopusDe struct {
|
||||
log *util.Logger
|
||||
gqlClient *octoDeGql.OctopusDeGraphQLClient
|
||||
gqlClient *krakengql.Client
|
||||
data *util.Monitor[api.Rates]
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ func buildOctopusDeFromConfig(other map[string]any) (*OctopusDe, error) {
|
|||
log := util.NewLogger("octopus-de")
|
||||
|
||||
// Create GraphQL client
|
||||
gqlClient, err := octoDeGql.NewClient(log, cc.Email, cc.Password, cc.AccountNumber)
|
||||
gqlClient, err := krakengql.NewClient(log, krakengql.BaseURI, cc.Email, cc.Password, cc.AccountNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ func (t *OctopusDe) run(done chan error) {
|
|||
if err := backoff.Retry(func() error {
|
||||
agr, err := t.gqlClient.ActiveAgreement()
|
||||
if err != nil {
|
||||
if errors.Is(err, octoDeGql.ErrAuthFailed) {
|
||||
if errors.Is(err, krakengql.ErrAuthFailed) {
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
return backoffPermanentError(err)
|
||||
|
|
@ -157,7 +157,7 @@ type RatePeriod struct {
|
|||
// ratesForAgreement determines the tariff type of agr and returns the corresponding
|
||||
// rate periods. It supports Dynamic, Simple, and Time-of-Use tariffs.
|
||||
// now is used as the reference time for horizon computation and ToU rate generation.
|
||||
func ratesForAgreement(agr octoDeGql.Agreement, now time.Time) ([]RatePeriod, error) {
|
||||
func ratesForAgreement(agr krakengql.Agreement, now time.Time) ([]RatePeriod, error) {
|
||||
horizon, err := computeHorizon(now, agr, planDays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -188,7 +188,7 @@ func ratesForAgreement(agr octoDeGql.Agreement, now time.Time) ([]RatePeriod, er
|
|||
}
|
||||
|
||||
// extractForecastRates converts dynamic-tariff UnitRateForecast entries into RatePeriod values.
|
||||
func extractForecastRates(forecasts []octoDeGql.UnitRateForecast, horizon planningHorizon) ([]RatePeriod, error) {
|
||||
func extractForecastRates(forecasts []krakengql.UnitRateForecast, horizon planningHorizon) ([]RatePeriod, error) {
|
||||
var rates []RatePeriod
|
||||
for _, forecast := range forecasts {
|
||||
info := forecast.UnitRateInformation
|
||||
|
|
@ -229,7 +229,7 @@ func extractForecastRates(forecasts []octoDeGql.UnitRateForecast, horizon planni
|
|||
|
||||
// simpleRates converts a SimpleProductUnitRateInformation into a single RatePeriod
|
||||
// ending at horizon, the pre-computed planning horizon.
|
||||
func simpleRates(info octoDeGql.SimpleProductUnitRateInformation, horizon planningHorizon) ([]RatePeriod, error) {
|
||||
func simpleRates(info krakengql.SimpleProductUnitRateInformation, horizon planningHorizon) ([]RatePeriod, error) {
|
||||
netRate, err := parseFloat(info.NetUnitRateCentsPerKwh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse net unit rate: %w", err)
|
||||
|
|
@ -247,7 +247,7 @@ func simpleRates(info octoDeGql.SimpleProductUnitRateInformation, horizon planni
|
|||
}
|
||||
|
||||
// computeHorizon returns the planning window, capped by the validity of the agreement.
|
||||
func computeHorizon(now time.Time, agreement octoDeGql.Agreement, planDays int) (planningHorizon, error) {
|
||||
func computeHorizon(now time.Time, agreement krakengql.Agreement, planDays int) (planningHorizon, error) {
|
||||
start := now
|
||||
end := now.AddDate(0, 0, planDays)
|
||||
|
||||
|
|
@ -289,7 +289,7 @@ func computePeriod(day time.Time, fromOffset, toOffset time.Duration) (time.Time
|
|||
|
||||
// ratePeriodsForDay expands one TouRate slot for a single day into RatePeriods,
|
||||
// filtered to the window [now, horizon].
|
||||
func ratePeriodsForDay(day time.Time, horizon planningHorizon, r octoDeGql.TouRate) ([]RatePeriod, error) {
|
||||
func ratePeriodsForDay(day time.Time, horizon planningHorizon, r krakengql.TouRate) ([]RatePeriod, error) {
|
||||
grossRate, err := parseFloat(r.LatestGrossUnitRateCentsPerKwh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse gross unit rate for slot %q: %w", r.TimeslotName, err)
|
||||
|
|
@ -328,7 +328,7 @@ func ratePeriodsForDay(day time.Time, horizon planningHorizon, r octoDeGql.TouRa
|
|||
// generateTouRates produces rate periods for a Time of Use tariff
|
||||
// by repeating each timeslot's activation window for each day in the planning horizon.
|
||||
// now is the reference time for filtering past periods; horizon is the pre-computed end of the window.
|
||||
func generateTouRates(rates []octoDeGql.TouRate, horizon planningHorizon) ([]RatePeriod, error) {
|
||||
func generateTouRates(rates []krakengql.TouRate, horizon planningHorizon) ([]RatePeriod, error) {
|
||||
startDay := now.With(horizon.start).BeginningOfDay()
|
||||
|
||||
var result []RatePeriod
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
octoDeGql "github.com/evcc-io/evcc/tariff/octopusde/graphql"
|
||||
krakengql "github.com/evcc-io/evcc/tariff/octopuskraken/graphql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -52,19 +52,19 @@ var t0 = time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
|
|||
// dynamicAgreement builds an agreement that uses a dynamic tariff:
|
||||
// the unitRateForecast field contains two half-hour forecast slots with
|
||||
// per-slot prices stored in TimeOfUseProductUnitRateInformation.
|
||||
func dynamicAgreement() octoDeGql.Agreement {
|
||||
func dynamicAgreement() krakengql.Agreement {
|
||||
t1 := t0
|
||||
t2 := t0.Add(15 * time.Minute)
|
||||
t3 := t2.Add(15 * time.Minute)
|
||||
return octoDeGql.Agreement{
|
||||
return krakengql.Agreement{
|
||||
IsActive: true,
|
||||
UnitRateForecast: []octoDeGql.UnitRateForecast{
|
||||
UnitRateForecast: []krakengql.UnitRateForecast{
|
||||
{
|
||||
ValidFrom: t1,
|
||||
ValidTo: t2,
|
||||
UnitRateInformation: octoDeGql.ForecastUnitRateInformation{
|
||||
TimeOfUseProductUnitRateInformation: octoDeGql.TimeOfUseProductUnitRateInformation{
|
||||
Rates: []octoDeGql.Rate{
|
||||
UnitRateInformation: krakengql.ForecastUnitRateInformation{
|
||||
TimeOfUseProductUnitRateInformation: krakengql.TimeOfUseProductUnitRateInformation{
|
||||
Rates: []krakengql.Rate{
|
||||
{NetUnitRateCentsPerKwh: "10.50", LatestGrossUnitRateCentsPerKwh: "12.495"},
|
||||
},
|
||||
},
|
||||
|
|
@ -73,9 +73,9 @@ func dynamicAgreement() octoDeGql.Agreement {
|
|||
{
|
||||
ValidFrom: t2,
|
||||
ValidTo: t3,
|
||||
UnitRateInformation: octoDeGql.ForecastUnitRateInformation{
|
||||
TimeOfUseProductUnitRateInformation: octoDeGql.TimeOfUseProductUnitRateInformation{
|
||||
Rates: []octoDeGql.Rate{
|
||||
UnitRateInformation: krakengql.ForecastUnitRateInformation{
|
||||
TimeOfUseProductUnitRateInformation: krakengql.TimeOfUseProductUnitRateInformation{
|
||||
Rates: []krakengql.Rate{
|
||||
{NetUnitRateCentsPerKwh: "8.00", LatestGrossUnitRateCentsPerKwh: "9.52"},
|
||||
},
|
||||
},
|
||||
|
|
@ -86,13 +86,13 @@ func dynamicAgreement() octoDeGql.Agreement {
|
|||
}
|
||||
|
||||
// simpleAgreement builds an agreement with a single fixed rate covering one year.
|
||||
func simpleAgreement() octoDeGql.Agreement {
|
||||
return octoDeGql.Agreement{
|
||||
func simpleAgreement() krakengql.Agreement {
|
||||
return krakengql.Agreement{
|
||||
IsActive: true,
|
||||
ValidFrom: t0,
|
||||
ValidTo: t0.AddDate(1, 0, 0),
|
||||
UnitRateInformation: octoDeGql.AgreementUnitRateInformation{
|
||||
SimpleProductUnitRateInformation: octoDeGql.SimpleProductUnitRateInformation{
|
||||
UnitRateInformation: krakengql.AgreementUnitRateInformation{
|
||||
SimpleProductUnitRateInformation: krakengql.SimpleProductUnitRateInformation{
|
||||
NetUnitRateCentsPerKwh: "25.00",
|
||||
LatestGrossUnitRateCentsPerKwh: "29.75",
|
||||
},
|
||||
|
|
@ -103,17 +103,17 @@ func simpleAgreement() octoDeGql.Agreement {
|
|||
// touAgreement builds an agreement with a two-slot time-of-use tariff:
|
||||
// - Day rate 06:00–22:00
|
||||
// - Night rate 22:00–06:00 (wraps past midnight)
|
||||
func touAgreement() octoDeGql.Agreement {
|
||||
return octoDeGql.Agreement{
|
||||
func touAgreement() krakengql.Agreement {
|
||||
return krakengql.Agreement{
|
||||
IsActive: true,
|
||||
UnitRateInformation: octoDeGql.AgreementUnitRateInformation{
|
||||
TimeOfUseProductUnitRateInformation: octoDeGql.TouAgreementUnitRateInformation{
|
||||
Rates: []octoDeGql.TouRate{
|
||||
UnitRateInformation: krakengql.AgreementUnitRateInformation{
|
||||
TimeOfUseProductUnitRateInformation: krakengql.TouAgreementUnitRateInformation{
|
||||
Rates: []krakengql.TouRate{
|
||||
{
|
||||
TimeslotName: "Day",
|
||||
NetUnitRateCentsPerKwh: "30.00",
|
||||
LatestGrossUnitRateCentsPerKwh: "35.70",
|
||||
TimeslotActivationRules: []octoDeGql.TimeslotActivationRule{
|
||||
TimeslotActivationRules: []krakengql.TimeslotActivationRule{
|
||||
{ActiveFromTime: "06:00:00", ActiveToTime: "22:00:00"},
|
||||
},
|
||||
},
|
||||
|
|
@ -121,7 +121,7 @@ func touAgreement() octoDeGql.Agreement {
|
|||
TimeslotName: "Night",
|
||||
NetUnitRateCentsPerKwh: "15.00",
|
||||
LatestGrossUnitRateCentsPerKwh: "17.85",
|
||||
TimeslotActivationRules: []octoDeGql.TimeslotActivationRule{
|
||||
TimeslotActivationRules: []krakengql.TimeslotActivationRule{
|
||||
// 22:00 → 06:00 wraps past midnight
|
||||
{ActiveFromTime: "22:00:00", ActiveToTime: "06:00:00"},
|
||||
},
|
||||
|
|
|
|||
134
tariff/octopusit.go
Normal file
134
tariff/octopusit.go
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
package tariff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
krakengql "github.com/evcc-io/evcc/tariff/octopuskraken/graphql"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// OctopusIt is an api.Tariff implementation for Octopus Energy Italy, reusing
|
||||
// the Germany implementation's Kraken GraphQL client and rate computation.
|
||||
type OctopusIt struct {
|
||||
log *util.Logger
|
||||
gqlClient *krakengql.Client
|
||||
data *util.Monitor[api.Rates]
|
||||
}
|
||||
|
||||
var _ api.Tariff = (*OctopusIt)(nil)
|
||||
|
||||
func init() {
|
||||
registry.Add("octopus-it", NewOctopusItFromConfig)
|
||||
}
|
||||
|
||||
// NewOctopusItFromConfig creates the tariff provider from the given config map, and runs it.
|
||||
func NewOctopusItFromConfig(other map[string]any) (api.Tariff, error) {
|
||||
t, err := buildOctopusItFromConfig(other)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return runOrError(t)
|
||||
}
|
||||
|
||||
// buildOctopusItFromConfig creates the Tariff provider from the given config map.
|
||||
// Split out to allow for testing.
|
||||
func buildOctopusItFromConfig(other map[string]any) (*OctopusIt, error) {
|
||||
var cc struct {
|
||||
Email string
|
||||
Password string
|
||||
AccountNumber string
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.Email == "" {
|
||||
return nil, errors.New("missing email")
|
||||
}
|
||||
|
||||
if cc.Password == "" {
|
||||
return nil, errors.New("missing password")
|
||||
}
|
||||
|
||||
if cc.AccountNumber == "" {
|
||||
return nil, errors.New("missing account number")
|
||||
}
|
||||
|
||||
log := util.NewLogger("octopus-it")
|
||||
|
||||
gqlClient, err := krakengql.NewClient(log, krakengql.ItBaseURI, cc.Email, cc.Password, cc.AccountNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &OctopusIt{
|
||||
log: log,
|
||||
gqlClient: gqlClient,
|
||||
data: util.NewMonitor[api.Rates](2 * time.Hour),
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *OctopusIt) run(done chan error) {
|
||||
var once sync.Once
|
||||
|
||||
for tick := time.Tick(time.Hour); ; <-tick {
|
||||
var rates []RatePeriod
|
||||
|
||||
if err := backoff.Retry(func() error {
|
||||
agr, err := t.gqlClient.ActiveAgreement()
|
||||
if err != nil {
|
||||
if errors.Is(err, krakengql.ErrAuthFailed) {
|
||||
return backoff.Permanent(err)
|
||||
}
|
||||
return backoffPermanentError(err)
|
||||
}
|
||||
rates, err = ratesForAgreement(agr, time.Now())
|
||||
return backoffPermanentError(err)
|
||||
}, bo()); err != nil {
|
||||
if reportError(&once, done, err) {
|
||||
return
|
||||
}
|
||||
|
||||
t.log.ERROR.Printf("failed to fetch unit rate forecast: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
data := make(api.Rates, 0, len(rates))
|
||||
for _, r := range rates {
|
||||
ar := api.Rate{
|
||||
Start: r.ValidFrom,
|
||||
End: r.ValidTo,
|
||||
// Convert from cents per kWh to € per kWh (divide by 100)
|
||||
// Use gross price (including tax) as that's what the customer pays
|
||||
Value: r.GrossUnitRateCentsPerKwh / 100,
|
||||
}
|
||||
data = append(data, ar)
|
||||
}
|
||||
|
||||
mergeRates(t.data, data)
|
||||
once.Do(func() { close(done) })
|
||||
}
|
||||
}
|
||||
|
||||
// Rates implements the api.Tariff interface
|
||||
func (t *OctopusIt) 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 *OctopusIt) Type() api.TariffType {
|
||||
return api.TariffTypePriceForecast
|
||||
}
|
||||
43
tariff/octopusit_test.go
Normal file
43
tariff/octopusit_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package tariff
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOctopusItConfigParse(t *testing.T) {
|
||||
validConfig := map[string]any{
|
||||
"email": "test@example.com",
|
||||
"password": "testpassword",
|
||||
"accountNumber": "A-12345678",
|
||||
}
|
||||
|
||||
tariff, err := buildOctopusItFromConfig(validConfig)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tariff)
|
||||
|
||||
missingEmailConfig := map[string]any{
|
||||
"password": "testpassword",
|
||||
"accountNumber": "A-12345678",
|
||||
}
|
||||
_, err = buildOctopusItFromConfig(missingEmailConfig)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing email")
|
||||
|
||||
missingPasswordConfig := map[string]any{
|
||||
"email": "test@example.com",
|
||||
"accountNumber": "A-12345678",
|
||||
}
|
||||
_, err = buildOctopusItFromConfig(missingPasswordConfig)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing password")
|
||||
|
||||
missingAccountNumberConfig := map[string]any{
|
||||
"email": "test@example.com",
|
||||
"password": "testpassword",
|
||||
}
|
||||
_, err = buildOctopusItFromConfig(missingAccountNumberConfig)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "missing account number")
|
||||
}
|
||||
|
|
@ -17,17 +17,22 @@ import (
|
|||
// The implementation in this file follows the published example at https://octopusenergy.de/blog/wohnen/dynamisch-sparen-per-api
|
||||
const BaseURI = "https://api.oeg-kraken.energy/v1/graphql/"
|
||||
|
||||
// OctopusDeGraphQLClient provides an interface for communicating with Octopus Energy Germany's Kraken platform.
|
||||
type OctopusDeGraphQLClient struct {
|
||||
// ItBaseURI is Octopus Energy Italy's Kraken API root, the same platform under its own domain.
|
||||
const ItBaseURI = "https://api.oeit-kraken.energy/v1/graphql/"
|
||||
|
||||
// Client provides an interface for communicating with an Octopus Energy Kraken platform instance.
|
||||
type Client struct {
|
||||
log *util.Logger
|
||||
*graphql.Client
|
||||
accountNumber string
|
||||
}
|
||||
|
||||
// NewClient returns a new, authenticated instance of OctopusDeGraphQLClient.
|
||||
func NewClient(log *util.Logger, email, password, accountNumber string) (*OctopusDeGraphQLClient, error) {
|
||||
// NewClient returns a new, authenticated instance for the given Kraken instance
|
||||
// (other regional Octopus companies run the same platform under their own baseURI).
|
||||
func NewClient(log *util.Logger, baseURI, email, password, accountNumber string) (*Client, error) {
|
||||
ts := oauth2.ReuseTokenSource(nil, &tokenSource{
|
||||
log: log,
|
||||
baseURI: baseURI,
|
||||
email: email,
|
||||
password: password,
|
||||
})
|
||||
|
|
@ -46,17 +51,17 @@ func NewClient(log *util.Logger, email, password, accountNumber string) (*Octopu
|
|||
Base: cli.Transport,
|
||||
}
|
||||
|
||||
gq := &OctopusDeGraphQLClient{
|
||||
gq := &Client{
|
||||
log: log,
|
||||
accountNumber: accountNumber,
|
||||
Client: graphql.NewClient(BaseURI, cli),
|
||||
Client: graphql.NewClient(baseURI, cli),
|
||||
}
|
||||
|
||||
return gq, nil
|
||||
}
|
||||
|
||||
// ActiveAgreement queries the Kraken API and returns the active electricity supply agreement.
|
||||
func (c *OctopusDeGraphQLClient) ActiveAgreement() (Agreement, error) {
|
||||
func (c *Client) ActiveAgreement() (Agreement, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ var ErrAuthFailed = errors.New("authentication failed")
|
|||
|
||||
type tokenSource struct {
|
||||
log *util.Logger
|
||||
baseURI string
|
||||
email, password string
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ func (ts *tokenSource) Token() (*oauth2.Token, error) {
|
|||
|
||||
// Create a temporary client without authentication for the token request
|
||||
cli := request.NewClient(ts.log)
|
||||
tempClient := graphql.NewClient(BaseURI, cli)
|
||||
tempClient := graphql.NewClient(ts.baseURI, cli)
|
||||
|
||||
var q krakenTokenAuthentication
|
||||
if err := tempClient.Mutate(ctx, &q, map[string]any{
|
||||
43
templates/definition/tariff/octopus-it.yaml
Normal file
43
templates/definition/tariff/octopus-it.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
template: octopus-it
|
||||
products:
|
||||
- brand: Octopus Energy
|
||||
description:
|
||||
de: Italien
|
||||
en: Italy
|
||||
requirements:
|
||||
evcc: ["skiptest"]
|
||||
countries: ["IT"]
|
||||
group: price
|
||||
params:
|
||||
- name: email
|
||||
type: string
|
||||
required: true
|
||||
example: "user@example.com"
|
||||
description:
|
||||
en: Email Address
|
||||
de: E-Mail-Adresse
|
||||
help:
|
||||
de: "Die E-Mail-Adresse Ihres Octopus Energy Kontos."
|
||||
en: "The email address of your Octopus Energy account."
|
||||
- name: password
|
||||
type: string
|
||||
required: true
|
||||
example: "secret"
|
||||
help:
|
||||
de: "Das Passwort Ihres Octopus Energy Kontos."
|
||||
en: "The password of your Octopus Energy account."
|
||||
- name: accountNumber
|
||||
type: string
|
||||
required: true
|
||||
example: "A-XX345678"
|
||||
description:
|
||||
en: Account Number
|
||||
de: Kundennummer
|
||||
help:
|
||||
de: "Ihre Octopus Energy Kundennummer (z.B. A-12345678)."
|
||||
en: "Your Octopus Energy account number (e.g., A-12345678)."
|
||||
render: |
|
||||
type: octopus-it
|
||||
accountNumber: {{ .accountNumber }}
|
||||
email: {{ .email }}
|
||||
password: {{ .password }}
|
||||
53
templates/definition/vehicle/octopus-it.yaml
Normal file
53
templates/definition/vehicle/octopus-it.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
template: octopus-it
|
||||
products:
|
||||
- brand: Octopus Energy
|
||||
description:
|
||||
de: Italien
|
||||
en: Italy
|
||||
countries: ["IT"]
|
||||
group: generic
|
||||
requirements:
|
||||
evcc: ["skiptest"]
|
||||
description:
|
||||
de: |
|
||||
Liest Fahrzeugdaten (Ladestand, Zielladestand) über die Octopus Energy Italy Kraken API.
|
||||
Das Fahrzeug bzw. die Wallbox muss im Octopus-Konto (Intelligent Octopus) verbunden sein.
|
||||
en: |
|
||||
Reads vehicle data (state of charge, target soc) via the Octopus Energy Italy Kraken API.
|
||||
The vehicle or charge point must be connected in the Octopus account (Intelligent Octopus).
|
||||
params:
|
||||
- preset: vehicle-common
|
||||
- preset: vehicle-online
|
||||
- name: welcomecharge
|
||||
advanced: true
|
||||
- name: email
|
||||
required: true
|
||||
- name: password
|
||||
required: true
|
||||
- name: accountnumber
|
||||
example: "A-12345678"
|
||||
description:
|
||||
en: Account Number
|
||||
de: Kundennummer
|
||||
help:
|
||||
en: Account when several are linked (e.g. A-12345678).
|
||||
de: Konto, wenn mehrere verknüpft sind (z.B. A-12345678).
|
||||
- name: device
|
||||
description:
|
||||
en: Vehicle
|
||||
de: Fahrzeug
|
||||
service: octopus-it/devices?email={email}&password={password}&accountnumber={accountnumber}
|
||||
help:
|
||||
en: Vehicle when the account has more than one.
|
||||
de: Fahrzeug, wenn das Konto mehrere besitzt.
|
||||
- name: cache
|
||||
default: 15m
|
||||
render: |
|
||||
type: octopus-it
|
||||
{{- include "vehicle-common" . }}
|
||||
email: {{ .email }}
|
||||
password: {{ .password }}
|
||||
accountnumber: {{ .accountnumber }}
|
||||
device: {{ .device }}
|
||||
cache: {{ .cache }}
|
||||
{{ include "vehicle-features" . }}
|
||||
|
|
@ -7,17 +7,17 @@ import (
|
|||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/octopusde"
|
||||
"github.com/evcc-io/evcc/vehicle/octopuskraken"
|
||||
)
|
||||
|
||||
// OctopusDe is an api.Vehicle implementation for the Octopus Energy Germany Kraken API
|
||||
type OctopusDe struct {
|
||||
*embed
|
||||
*octopusde.API
|
||||
*octopuskraken.API
|
||||
account string
|
||||
device string
|
||||
deviceID string
|
||||
dataG func() (octopusde.Device, error)
|
||||
dataG func() (octopuskraken.Device, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -47,7 +47,7 @@ func NewOctopusDeFromConfig(other map[string]any) (api.Vehicle, error) {
|
|||
|
||||
log := util.NewLogger("octopus-de").Redact(cc.Email, cc.Password)
|
||||
|
||||
api, err := octopusde.NewAPI(log, cc.Email, cc.Password)
|
||||
api, err := octopuskraken.NewAPI(log, octopuskraken.BaseURI, cc.Email, cc.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -66,16 +66,16 @@ func NewOctopusDeFromConfig(other map[string]any) (api.Vehicle, error) {
|
|||
|
||||
// status fetches the live state of the configured device, resolving the account
|
||||
// and matching device on first use.
|
||||
func (v *OctopusDe) status() (octopusde.Device, error) {
|
||||
func (v *OctopusDe) status() (octopuskraken.Device, error) {
|
||||
account, err := v.Account(v.account)
|
||||
if err != nil {
|
||||
return octopusde.Device{}, err
|
||||
return octopuskraken.Device{}, err
|
||||
}
|
||||
v.account = account
|
||||
|
||||
devices, err := v.Devices(account)
|
||||
if err != nil {
|
||||
return octopusde.Device{}, err
|
||||
return octopuskraken.Device{}, err
|
||||
}
|
||||
|
||||
for _, d := range devices {
|
||||
|
|
@ -94,9 +94,9 @@ func (v *OctopusDe) status() (octopusde.Device, error) {
|
|||
}
|
||||
|
||||
if v.device == "" {
|
||||
return octopusde.Device{}, api.ErrNotAvailable
|
||||
return octopuskraken.Device{}, api.ErrNotAvailable
|
||||
}
|
||||
return octopusde.Device{}, fmt.Errorf("device not found: %s", v.device)
|
||||
return octopuskraken.Device{}, fmt.Errorf("device not found: %s", v.device)
|
||||
}
|
||||
|
||||
// Soc implements the api.Vehicle interface
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package octopusde
|
||||
package octopuskraken
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -6,25 +6,31 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
octoDeGql "github.com/evcc-io/evcc/tariff/octopusde/graphql"
|
||||
krakengql "github.com/evcc-io/evcc/tariff/octopuskraken/graphql"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// API is the Octopus Energy Germany Kraken client for vehicle data. It reuses the
|
||||
// authenticated Kraken GraphQL client from the tariff implementation so the JWT
|
||||
// token source and auth transport are not duplicated.
|
||||
// BaseURI re-exports the tariff graphql package's Kraken API root.
|
||||
const BaseURI = krakengql.BaseURI
|
||||
|
||||
// ItBaseURI re-exports the tariff graphql package's Italian Kraken API root.
|
||||
const ItBaseURI = krakengql.ItBaseURI
|
||||
|
||||
// API is the Octopus Energy Kraken client for vehicle data, reusing the
|
||||
// authenticated Kraken GraphQL client from the tariff implementation.
|
||||
type API struct {
|
||||
*octoDeGql.OctopusDeGraphQLClient
|
||||
*krakengql.Client
|
||||
}
|
||||
|
||||
// NewAPI creates a Kraken API client authenticated via the given credentials.
|
||||
func NewAPI(log *util.Logger, email, password string) (*API, error) {
|
||||
// NewAPI creates a Kraken API client for the given Kraken instance (other
|
||||
// regional Octopus companies run the same platform under their own baseURI).
|
||||
func NewAPI(log *util.Logger, baseURI, email, password string) (*API, error) {
|
||||
// the account number is discovered on demand and not needed for the shared client
|
||||
client, err := octoDeGql.NewClient(log, email, password, "")
|
||||
client, err := krakengql.NewClient(log, baseURI, email, password, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &API{OctopusDeGraphQLClient: client}, nil
|
||||
return &API{Client: client}, nil
|
||||
}
|
||||
|
||||
// krakenAccounts lists the accounts accessible to the authenticated user.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package octopusde
|
||||
package octopuskraken
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package octopusde
|
||||
package octopuskraken
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -10,15 +10,23 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /devices", getDevices)
|
||||
register("octopus-de", BaseURI)
|
||||
register("octopus-it", ItBaseURI)
|
||||
}
|
||||
|
||||
service.Register("octopus-de", mux)
|
||||
// register wires the device-listing endpoint for a Kraken instance under the given service name.
|
||||
func register(name, baseURI string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /devices", func(w http.ResponseWriter, req *http.Request) {
|
||||
getDevices(w, req, name, baseURI)
|
||||
})
|
||||
|
||||
service.Register(name, mux)
|
||||
}
|
||||
|
||||
// getDevices lists the ids of the SmartFlex devices in the account, driving
|
||||
// device selection in the template.
|
||||
func getDevices(w http.ResponseWriter, req *http.Request) {
|
||||
func getDevices(w http.ResponseWriter, req *http.Request, name, baseURI string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
q := req.URL.Query()
|
||||
|
|
@ -31,8 +39,8 @@ func getDevices(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
log := util.NewLogger("octopus-de").Redact(email, password)
|
||||
api, err := NewAPI(log, email, password)
|
||||
log := util.NewLogger(name).Redact(email, password)
|
||||
api, err := NewAPI(log, baseURI, email, password)
|
||||
if err != nil {
|
||||
log.ERROR.Println(err)
|
||||
return
|
||||
Loading…
Add table
Add a link
Reference in a new issue