evcc-io/vehicle/tibber.go

172 lines
3.7 KiB
Go

package vehicle
import (
"context"
"fmt"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle/tibber"
)
// Tibber is an api.Vehicle implementation for the Tibber Data API
type Tibber struct {
*embed
*tibber.API
vin string
homeID string
devID string
dataG func() (tibber.DeviceDetail, error)
}
func init() {
registry.AddCtx("tibber", NewTibberFromConfig)
}
// NewTibberFromConfig creates a new vehicle
func NewTibberFromConfig(ctx context.Context, other map[string]any) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
ClientID, ClientSecret, RedirectURI string
VIN string
Cache time.Duration
}{
Cache: interval,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.ClientID == "" || cc.ClientSecret == "" {
return nil, api.ErrMissingCredentials
}
log := util.NewLogger("tibber").Redact(cc.ClientID, cc.ClientSecret)
authCtx := util.WithLogger(context.Background(), log)
ts, err := tibber.NewOAuth(authCtx, cc.ClientID, cc.ClientSecret, cc.RedirectURI, cc.embed.GetTitle())
if err != nil {
return nil, err
}
v := &Tibber{
embed: &cc.embed,
API: tibber.NewAPI(log, ts),
vin: strings.ToUpper(cc.VIN),
}
v.dataG = util.Cached(v.status, cc.Cache)
return v, nil
}
// resolve discovers the home and device id of the configured vehicle. It is
// deferred until first use because authorization happens interactively. With
// only the vehicles scope granted, the devices endpoint returns vehicles only.
func (v *Tibber) resolve() error {
homes, err := v.Homes()
if err != nil {
return err
}
for _, home := range homes {
devices, err := v.Devices(home.ID)
if err != nil {
return err
}
for _, d := range devices {
// match the bare vin or the full external id (e.g. tesla:5YJ...)
if v.vin == "" || strings.EqualFold(d.VIN(), v.vin) || strings.EqualFold(d.ExternalID, v.vin) {
v.homeID = home.ID
v.devID = d.ID
return nil
}
}
}
return fmt.Errorf("vehicle not found: %s", v.vin)
}
func (v *Tibber) status() (tibber.DeviceDetail, error) {
if v.devID == "" {
if err := v.resolve(); err != nil {
return tibber.DeviceDetail{}, err
}
}
return v.Device(v.homeID, v.devID)
}
// Soc implements the api.Vehicle interface
func (v *Tibber) Soc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
soc, ok := res.Soc()
if !ok {
return 0, api.ErrNotAvailable
}
return soc, nil
}
var _ api.ChargeState = (*Tibber)(nil)
// Status implements the api.ChargeState interface
func (v *Tibber) Status() (api.ChargeStatus, error) {
res, err := v.dataG()
if err != nil {
return api.StatusNone, err
}
status := api.StatusA // disconnected
if plug, ok := res.PlugStatus(); ok && plug == tibber.StatusConnected {
status = api.StatusB // connected, not charging
}
if charging, ok := res.ChargingStatus(); ok && charging == tibber.StatusCharging {
status = api.StatusC // charging
}
return status, nil
}
var _ api.SocLimiter = (*Tibber)(nil)
// GetLimitSoc implements the api.SocLimiter interface
func (v *Tibber) GetLimitSoc() (int64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
soc, ok := res.TargetSoc()
if !ok {
return 0, api.ErrNotAvailable
}
return int64(soc), nil
}
var _ api.VehicleRange = (*Tibber)(nil)
// Range implements the api.VehicleRange interface
func (v *Tibber) Range() (int64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
rng, ok := res.Range()
if !ok {
return 0, api.ErrNotAvailable
}
return int64(rng), nil
}