Tesla BLE: handle sleeping vehicles with vehicle-api charger (#32615)

This commit is contained in:
Jérôme DAVID 2026-08-09 13:39:24 +02:00 • committed by GitHub
parent 9ef7ea748a
commit c1fac30171
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 98 additions and 7 deletions

View file

@ -1,13 +1,17 @@
package charger
import (
"encoding/json"
"errors"
"math"
"net/http"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
)
// VehicleApi is a charger implementation that uses the vehicle api
@ -49,6 +53,28 @@ func NewVehicleApiFromConfig(other map[string]any) (api.Charger, error) {
return c, nil
}
// asleep maps a vehicle api's sleeping response to api.ErrAsleep so the loadpoint
// can trigger the existing wake-up logic. Proxies like TeslaBleHttpProxy answer
// HTTP 503 with a json body stating the reason.
func asleep(err error) error {
var se *request.StatusError
if !errors.As(err, &se) || !se.HasStatus(http.StatusServiceUnavailable) {
return err
}
var res struct {
Response struct {
Reason string
}
}
if json.Unmarshal(se.Body(), &res) == nil && strings.Contains(strings.ToLower(res.Response.Reason), "sleep") {
return api.ErrAsleep
}
return err
}
// isVehicleAtHome checks if the vehicle is within the geofence (if enabled)
func (c *VehicleApi) isVehicleAtHome(vehicle api.Vehicle) (bool, error) {
if !c.geofenceEnabled {
@ -62,7 +88,7 @@ func (c *VehicleApi) isVehicleAtHome(vehicle api.Vehicle) (bool, error) {
lat, lon, err := v.Position()
if err != nil {
return false, err
return false, asleep(err)
}
return c.distance(lat, lon) <= c.radius, nil
@ -82,6 +108,11 @@ func (c *VehicleApi) Status() (api.ChargeStatus, error) {
// Check if vehicle is at the charger (trying to use geofencing)
atHome, err := c.isVehicleAtHome(vehicle)
if err != nil {
// position unknown while asleep: report connected so the loadpoint wakes the
// vehicle, the geofence is re-evaluated once it responds again
if errors.Is(err, api.ErrAsleep) {
return api.StatusB, nil
}
return api.StatusA, err
}
@ -104,6 +135,10 @@ func (c *VehicleApi) Status() (api.ChargeStatus, error) {
status, err := v.Status()
if err != nil {
// asleep: report connected so the loadpoint's setLimit path can wake the vehicle
if errors.Is(asleep(err), api.ErrAsleep) {
return api.StatusB, nil
}
return api.StatusNone, err
}
@ -142,7 +177,7 @@ func (c *VehicleApi) Enable(enable bool) error {
}
if err := v.ChargeEnable(enable); err != nil {
return err
return asleep(err)
}
c.enabled = enable
@ -164,7 +199,7 @@ func (c *VehicleApi) MaxCurrent(current int64) error {
return nil
}
return v.MaxCurrent(current)
return asleep(v.MaxCurrent(current))
}
var _ api.Resurrector = (*VehicleApi)(nil)

View file

@ -0,0 +1,49 @@
package charger
import (
"io"
"net/http"
"strings"
"testing"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util/request"
"github.com/stretchr/testify/require"
)
// statusErr builds the error a plugin returns for the given response
func statusErr(t *testing.T, code int, body string) error {
t.Helper()
req, err := http.NewRequest(http.MethodGet, "http://vehicle.test", nil)
require.NoError(t, err)
resp := &http.Response{
StatusCode: code,
Body: io.NopCloser(strings.NewReader(body)),
Request: req,
}
_, err = request.ReadBody(resp)
require.Error(t, err)
return err
}
func TestAsleep(t *testing.T) {
sleeping := `{"response":{"result":false,"reason":"vehicle is sleeping"}}`
// TeslaBleHttpProxy sleeping response
require.ErrorIs(t, asleep(statusErr(t, http.StatusServiceUnavailable, sleeping)), api.ErrAsleep)
// 503 for any other reason is not asleep
other := statusErr(t, http.StatusServiceUnavailable, `{"response":{"reason":"vehicle is offline"}}`)
require.NotErrorIs(t, asleep(other), api.ErrAsleep)
// non-503 with sleeping body is not asleep
require.NotErrorIs(t, asleep(statusErr(t, http.StatusInternalServerError, sleeping)), api.ErrAsleep)
// non-json body and nil error pass through
require.NotErrorIs(t, asleep(statusErr(t, http.StatusServiceUnavailable, "sleeping")), api.ErrAsleep)
require.NoError(t, asleep(nil))
}