Add Tessie (#19881)

This commit is contained in:
fanatix 2025-03-23 11:14:58 +01:00 • committed by GitHub
parent 0d658e3381
commit 9f87106b40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 302 additions and 1 deletions

273
charger/tessie.go Normal file
View file

@ -0,0 +1,273 @@
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.Add("tessie", NewTessieFromConfig)
}
func NewTessieFromConfig(other map[string]interface{}) (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(context.Background(), 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) {
locationName, err := t.getLocationName()
if err != nil {
return false, err
}
locationMatch := t.Location == locationName || t.Location == "always"
if !locationMatch {
return false, nil
}
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
}
_, err = t.client.Do(req)
if err != nil {
return err
}
if !enable {
err = t.MaxCurrent(32)
if err != nil {
return err
}
}
return nil
}
func (t *Tessie) MaxCurrent(current int64) error {
locationName, err := t.getLocationName()
if err != nil {
return err
}
locationMatch := t.Location == locationName || t.Location == "always"
if !locationMatch {
return nil
}
url := fmt.Sprintf("https://api.tessie.com/%s/command/set_charging_amps?retry_duration=40&wait_for_completion=true&amps=%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) {
locationName, err := t.getLocationName()
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
}
locationMatch := t.Location == locationName || t.Location == "always"
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) {
locationName, err := t.getLocationName()
if err != nil {
return 0, err
}
locationMatch := t.Location == locationName || t.Location == "always"
if !locationMatch {
return 0, nil
}
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) {
locationName, err := t.getLocationName()
if err != nil {
return 0, err
}
locationMatch := t.Location == locationName || t.Location == "always"
if !locationMatch {
return 0, nil
}
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) getLocationName() (string, error) {
url := fmt.Sprintf("https://api.tessie.com/%s/location", t.Vin)
req, err := request.New(http.MethodGet, url, nil, nil)
if err != nil {
return "", 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 "", err
}
return res.SavedLocation, nil
}

View file

@ -0,0 +1,28 @@
template: tessie
group: generic
products:
- description:
generic: Tessie
requirements:
description:
en: Charger connected via the Tessie API. Allows control of charging state and configuration of maximum current.
de: Ladegerät, das über die Tessie-API verbunden ist. Ermöglicht die Steuerung des Ladezustands und die Konfiguration des maximalen Stroms.
params:
- name: vin
required: true
- name: token
description:
de: Tessie API Token
en: Tessie API Token
required: true
- name: location
description:
de: Definieren Sie einen Ort, an dem das Tessie-Ladegerät funktioniert (always = immer, kein Geofence) oder ein benutzerdefinierter Ort (genauer Name), wie in Tessie definiert (Case sensitive)
en: Define a location where the Tessie charger will work (always = always, no geofence) or a custom location (exact name) as defined in Tessie (Case sensitive)
example: "tessiehome"
required: true
render: |
type: tessie
vin: {{ .vin }}
token: {{ .token }}
location: {{ .location }}

View file

@ -86,7 +86,7 @@ render: |
method: POST
wakeup:
source: http
uri: https://api.tessie.com/{{ .vin }}/wake
uri: https://api.tessie.com/{{ .vin }}/command/wake
headers:
Authorization: Bearer {{ .token }}
method: POST