239 lines
6.1 KiB
Go
239 lines
6.1 KiB
Go
package vehicle
|
|
|
|
// LICENSE
|
|
|
|
// Copyright (c) evcc.io (andig, naltatis, premultiply)
|
|
|
|
// This module is NOT covered by the MIT license. All rights reserved.
|
|
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/oauth"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"github.com/evcc-io/evcc/util/sponsor"
|
|
"github.com/evcc-io/evcc/vehicle/tronity"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// Tronity is an api.Vehicle implementation for the Tronity api
|
|
type Tronity struct {
|
|
*embed
|
|
*request.Helper
|
|
log *util.Logger
|
|
oc *oauth2.Config
|
|
vid string
|
|
bulkG func() (tronity.Bulk, error)
|
|
}
|
|
|
|
func init() {
|
|
registry.Add("tronity", NewTronityFromConfig)
|
|
}
|
|
|
|
//go:generate go tool decorate -f decorateTronity -b *Tronity -r api.Vehicle -t "api.ChargeState,Status,func() (api.ChargeStatus, error)" -t "api.VehicleOdometer,Odometer,func() (float64, error)" -t "api.ChargeController,ChargeEnable,func(bool) error"
|
|
|
|
// NewTronityFromConfig creates a new vehicle
|
|
func NewTronityFromConfig(other map[string]any) (api.Vehicle, error) {
|
|
cc := struct {
|
|
embed `mapstructure:",squash"`
|
|
Credentials ClientCredentials
|
|
Tokens Tokens
|
|
VIN string
|
|
Cache time.Duration
|
|
}{
|
|
Cache: interval,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := cc.Credentials.Error(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !sponsor.IsAuthorized() {
|
|
return nil, api.ErrSponsorRequired
|
|
}
|
|
|
|
// authenticated http client with logging injected to the tronity client
|
|
log := util.NewLogger("tronity").Redact(cc.Credentials.ID, cc.Credentials.Secret)
|
|
|
|
oc, err := tronity.OAuth2Config(cc.Credentials.ID, cc.Credentials.Secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
v := &Tronity{
|
|
log: log,
|
|
embed: &cc.embed,
|
|
Helper: request.NewHelper(log),
|
|
oc: oc,
|
|
}
|
|
|
|
var ts oauth2.TokenSource
|
|
token, err := cc.Tokens.Token()
|
|
|
|
// https://app.platform.tronity.io/docs#tag/Authentication
|
|
if err != nil {
|
|
// use app flow if we don't have tokens
|
|
ts = oauth.RefreshTokenSource(nil, v)
|
|
} else {
|
|
// use provided tokens generated by code flow
|
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
|
|
ts = oc.TokenSource(ctx, token)
|
|
}
|
|
|
|
// replace client transport with authenticated transport
|
|
v.Client.Transport = &oauth2.Transport{
|
|
Source: ts,
|
|
Base: v.Client.Transport,
|
|
}
|
|
|
|
vehicle, err := ensureVehicleEx(
|
|
cc.VIN, v.vehicles,
|
|
func(v tronity.Vehicle) (string, error) {
|
|
return v.VIN, nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
v.vid = vehicle.ID
|
|
v.bulkG = util.Cached(v.bulk, cc.Cache)
|
|
|
|
var status func() (api.ChargeStatus, error)
|
|
if slices.Contains(vehicle.Scopes, tronity.ReadCharge) {
|
|
status = v.status
|
|
}
|
|
|
|
var odometer func() (float64, error)
|
|
if slices.Contains(vehicle.Scopes, tronity.ReadOdometer) {
|
|
odometer = v.odometer
|
|
}
|
|
|
|
var chargeEnable func(bool) error
|
|
if slices.Contains(vehicle.Scopes, tronity.WriteChargeStartStop) {
|
|
chargeEnable = v.chargeEnable
|
|
}
|
|
|
|
return decorateTronity(v, status, odometer, chargeEnable), nil
|
|
}
|
|
|
|
// RefreshToken performs token refresh by logging in with app context
|
|
func (v *Tronity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
|
data := struct {
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
GrantType string `json:"grant_type"`
|
|
}{
|
|
ClientID: v.oc.ClientID,
|
|
ClientSecret: v.oc.ClientSecret,
|
|
GrantType: "app",
|
|
}
|
|
|
|
req, _ := request.New(http.MethodPost, v.oc.Endpoint.TokenURL, request.MarshalJSON(data), request.JSONEncoding)
|
|
|
|
var token oauth2.Token
|
|
err := request.NewHelper(v.log).DoJSON(req, &token)
|
|
|
|
return util.TokenWithExpiry(&token), err
|
|
}
|
|
|
|
// vehicles implements the vehicles api
|
|
func (v *Tronity) vehicles() ([]tronity.Vehicle, error) {
|
|
uri := fmt.Sprintf("%s/tronity/vehicles", tronity.URI)
|
|
|
|
var res tronity.Vehicles
|
|
err := v.GetJSON(uri, &res)
|
|
|
|
return res.Data, err
|
|
}
|
|
|
|
// bulk implements the bulk api
|
|
func (v *Tronity) bulk() (tronity.Bulk, error) {
|
|
uri := fmt.Sprintf("%s/tronity/vehicles/%s/last_record", tronity.URI, v.vid)
|
|
|
|
var res tronity.Bulk
|
|
err := v.GetJSON(uri, &res)
|
|
|
|
return res, err
|
|
}
|
|
|
|
// Soc implements the api.Vehicle interface
|
|
func (v *Tronity) Soc() (float64, error) {
|
|
res, err := v.bulkG()
|
|
return res.Level, err
|
|
}
|
|
|
|
// status implements the api.ChargeState interface
|
|
func (v *Tronity) status() (api.ChargeStatus, error) {
|
|
status := api.StatusA // disconnected
|
|
res, err := v.bulkG()
|
|
if err != nil {
|
|
return status, err
|
|
}
|
|
|
|
switch {
|
|
case res.Charging == "Charging":
|
|
status = api.StatusC
|
|
case res.Plugged:
|
|
status = api.StatusB
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
var _ api.VehicleRange = (*Tronity)(nil)
|
|
|
|
// Range implements the api.VehicleRange interface
|
|
func (v *Tronity) Range() (int64, error) {
|
|
res, err := v.bulkG()
|
|
return int64(res.Range), err
|
|
}
|
|
|
|
// odometer implements the api.VehicleOdometer interface
|
|
func (v *Tronity) odometer() (float64, error) {
|
|
res, err := v.bulkG()
|
|
return res.Odometer, err
|
|
}
|
|
|
|
func (v *Tronity) post(uri string) error {
|
|
resp, err := v.Post(uri, "", nil)
|
|
if err == nil {
|
|
err = request.ResponseError(resp)
|
|
}
|
|
|
|
// ignore HTTP 405
|
|
if se := new(request.StatusError); errors.As(err, &se) && se.StatusCode() == http.StatusMethodNotAllowed {
|
|
err = nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// chargeEnable implements the api.ChargeController interface
|
|
func (v *Tronity) chargeEnable(enable bool) error {
|
|
action := map[bool]string{true: "start_charging", false: "stop_charging"}
|
|
uri := fmt.Sprintf("%s/tronity/vehicles/%s/%s", tronity.URI, v.vid, action[enable])
|
|
return v.post(uri)
|
|
}
|