251 lines
5.7 KiB
Go
251 lines
5.7 KiB
Go
package charger
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
type Tessie struct {
|
|
log *util.Logger
|
|
client *request.Helper
|
|
Vin string
|
|
Location string
|
|
Maxcurrent int64
|
|
chargingStartedAfterLeavingGeofence bool
|
|
}
|
|
|
|
func init() {
|
|
registry.AddCtx("tessie", NewTessieFromConfig)
|
|
}
|
|
|
|
func NewTessieFromConfig(ctx context.Context, other map[string]any) (api.Charger, error) {
|
|
cc := struct {
|
|
Vin string
|
|
Token string
|
|
Location string
|
|
Maxcurrent int64
|
|
}{}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log := util.NewLogger("tessie")
|
|
|
|
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: cc.Token})
|
|
oauthClient := oauth2.NewClient(ctx, tokenSource)
|
|
|
|
client := request.NewHelper(log)
|
|
client.Client = oauthClient
|
|
|
|
t := &Tessie{
|
|
log: log,
|
|
client: client,
|
|
Vin: cc.Vin,
|
|
Location: cc.Location,
|
|
Maxcurrent: cc.Maxcurrent,
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func (t *Tessie) Enabled() (bool, error) {
|
|
if ok, err := t.locationMatch(); err != nil || !ok {
|
|
return false, err
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/state?values=charge_state", t.Vin)
|
|
req, err := request.New(http.MethodGet, url, nil, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var res struct {
|
|
ChargeState struct {
|
|
ChargingState string `json:"charging_state"`
|
|
} `json:"charge_state"`
|
|
}
|
|
|
|
if err := t.client.DoJSON(req, &res); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return res.ChargeState.ChargingState == "Charging", nil
|
|
}
|
|
|
|
func (t *Tessie) Enable(enable bool) error {
|
|
command := "stop_charging"
|
|
if enable {
|
|
command = "start_charging"
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/command/%s?retry_duration=40&wait_for_completion=true", t.Vin, command)
|
|
req, err := request.New(http.MethodPost, url, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := t.client.Do(req); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !enable {
|
|
if err := t.MaxCurrent(32); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *Tessie) MaxCurrent(current int64) error {
|
|
if ok, err := t.locationMatch(); err != nil || !ok {
|
|
return err
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/command/set_charging_amps?retry_duration=40&wait_for_completion=true&s=%d", t.Vin, current)
|
|
req, err := request.New(http.MethodPost, url, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = t.client.Do(req)
|
|
return err
|
|
}
|
|
|
|
func (t *Tessie) Status() (api.ChargeStatus, error) {
|
|
locationMatch, err := t.locationMatch()
|
|
if err != nil {
|
|
return api.StatusNone, err
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/state?values=charge_state", t.Vin)
|
|
req, err := request.New(http.MethodGet, url, nil, nil)
|
|
if err != nil {
|
|
return api.StatusNone, err
|
|
}
|
|
|
|
var res struct {
|
|
ChargeState struct {
|
|
ChargingState string `json:"charging_state"`
|
|
ChargePortDoorOpen bool `json:"charge_port_door_open"`
|
|
} `json:"charge_state"`
|
|
}
|
|
|
|
if err := t.client.DoJSON(req, &res); err != nil {
|
|
return api.StatusNone, err
|
|
}
|
|
|
|
if !locationMatch {
|
|
if !t.chargingStartedAfterLeavingGeofence {
|
|
if err := t.startCharging(); err != nil {
|
|
t.log.ERROR.Printf("Failed to start charging after leaving geofence: %v", err)
|
|
} else {
|
|
t.chargingStartedAfterLeavingGeofence = true
|
|
}
|
|
}
|
|
return api.StatusA, nil
|
|
} else {
|
|
t.chargingStartedAfterLeavingGeofence = false
|
|
}
|
|
|
|
switch res.ChargeState.ChargingState {
|
|
case "Charging":
|
|
return api.StatusC, nil
|
|
case "Complete":
|
|
return api.StatusA, nil
|
|
default:
|
|
if res.ChargeState.ChargePortDoorOpen {
|
|
return api.StatusB, nil
|
|
}
|
|
return api.StatusA, nil
|
|
}
|
|
}
|
|
|
|
func (t *Tessie) CurrentPower() (float64, error) {
|
|
if ok, err := t.locationMatch(); err != nil || !ok {
|
|
return 0, err
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/state?values=charge_state", t.Vin)
|
|
req, err := request.New(http.MethodGet, url, nil, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var res struct {
|
|
ChargeState struct {
|
|
ChargerPower float64 `json:"charger_power"`
|
|
} `json:"charge_state"`
|
|
}
|
|
|
|
if err := t.client.DoJSON(req, &res); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return res.ChargeState.ChargerPower * 1000, nil
|
|
}
|
|
|
|
func (t *Tessie) ChargedEnergy() (float64, error) {
|
|
if ok, err := t.locationMatch(); err != nil || !ok {
|
|
return 0, err
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/state?values=charge_state", t.Vin)
|
|
req, err := request.New(http.MethodGet, url, nil, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var res struct {
|
|
ChargeState struct {
|
|
ChargeEnergyAdded float64 `json:"charge_energy_added"`
|
|
} `json:"charge_state"`
|
|
}
|
|
|
|
if err := t.client.DoJSON(req, &res); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return res.ChargeState.ChargeEnergyAdded, nil
|
|
}
|
|
|
|
func (t *Tessie) startCharging() error {
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/command/start_charging?retry_duration=40&wait_for_completion=true", t.Vin)
|
|
req, err := request.New(http.MethodPost, url, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = t.client.Do(req)
|
|
return err
|
|
}
|
|
|
|
func (t *Tessie) locationMatch() (bool, error) {
|
|
if t.Location == "always" {
|
|
return true, nil
|
|
}
|
|
|
|
url := fmt.Sprintf("https://api.tessie.com/%s/location", t.Vin)
|
|
req, err := request.New(http.MethodGet, url, nil, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var res struct {
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
Address string `json:"address"`
|
|
SavedLocation string `json:"saved_location"`
|
|
}
|
|
|
|
if err := t.client.DoJSON(req, &res); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return t.Location == res.SavedLocation, nil
|
|
}
|