Add Octopus Energy Italy tariff (#31505)

This commit is contained in:
andig 2026-07-07 12:06:08 +02:00 • committed by GitHub
parent 8a771d5fb3
commit de976b7aa2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 360 additions and 67 deletions

View file

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

View file

@ -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
View 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
View 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")
}

View file

@ -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()

View file

@ -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{

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

View 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" . }}

View file

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

View file

@ -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.

View file

@ -1,4 +1,4 @@
package octopusde
package octopuskraken
import (
"testing"

View file

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