evcc-io/vehicle/octopuskraken/api.go

149 lines
4.2 KiB
Go

package octopuskraken
import (
"context"
"encoding/json"
"time"
"github.com/evcc-io/evcc/api"
krakengql "github.com/evcc-io/evcc/tariff/octopuskraken/graphql"
"github.com/evcc-io/evcc/util"
)
// 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 {
*krakengql.Client
}
// 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 := krakengql.NewClient(log, baseURI, email, password, "")
if err != nil {
return nil, err
}
return &API{Client: client}, nil
}
// krakenAccounts lists the accounts accessible to the authenticated user.
type krakenAccounts struct {
Viewer struct {
Accounts []struct {
Number string
}
}
}
// Account returns the configured account number, or the first account accessible
// to the authenticated user when none is configured.
func (v *API) Account(account string) (string, error) {
if account != "" {
return account, nil
}
accounts, err := v.Accounts()
if err != nil {
return "", err
}
if len(accounts) == 0 {
return "", api.ErrNotAvailable
}
return accounts[0], nil
}
// Accounts returns the account numbers accessible to the authenticated user.
func (v *API) Accounts() ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var q krakenAccounts
if err := v.Query(ctx, &q, nil); err != nil {
return nil, err
}
res := make([]string, 0, len(q.Viewer.Accounts))
for _, a := range q.Viewer.Accounts {
res = append(res, a.Number)
}
return res, nil
}
// socStatus holds the live state-of-charge values reported for a SmartFlex device.
// json.Number decodes both bare and quoted numbers (Kraken serialises soc as "41.00").
type socStatus struct {
StateOfCharge struct {
Value *json.Number
}
StateOfChargeLimit struct {
UpperSocLimit *json.Number
}
}
// Device is a SmartFlex device (vehicle or charge point) with its live status.
type Device struct {
ID string
Name string
DeviceType string
Provider string
Status struct {
// Both fragments select the same fields; the API returns whichever matches
// the device's concrete type, so reading either yields the live values.
Vehicle socStatus `graphql:"... on SmartFlexVehicleStatus"`
ChargePoint socStatus `graphql:"... on SmartFlexChargePointStatus"`
}
}
// soc returns the populated state-of-charge status for the device.
func (d Device) soc() socStatus {
if d.Status.Vehicle.StateOfCharge.Value != nil || d.Status.Vehicle.StateOfChargeLimit.UpperSocLimit != nil {
return d.Status.Vehicle
}
return d.Status.ChargePoint
}
// parseNumber returns the float value of a reported json.Number, if present and valid.
func parseNumber(n *json.Number) (float64, bool) {
if n == nil {
return 0, false
}
f, err := n.Float64()
return f, err == nil
}
// Soc returns the battery state of charge in percent, if reported.
func (d Device) Soc() (float64, bool) {
return parseNumber(d.soc().StateOfCharge.Value)
}
// TargetSoc returns the configured charge limit in percent, if any.
func (d Device) TargetSoc() (float64, bool) {
return parseNumber(d.soc().StateOfChargeLimit.UpperSocLimit)
}
// krakenDevices lists the SmartFlex devices of an account.
type krakenDevices struct {
Devices []Device `graphql:"devices(accountNumber: $accountNumber)"`
}
// Devices lists the SmartFlex devices of the given account.
func (v *API) Devices(accountNumber string) ([]Device, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var q krakenDevices
if err := v.Query(ctx, &q, map[string]any{
"accountNumber": accountNumber,
}); err != nil {
return nil, err
}
return q.Devices, nil
}