Tesla BLE: handle sleeping vehicles with vehicle-api charger (#32615)
This commit is contained in:
parent
9ef7ea748a
commit
c1fac30171
3 changed files with 98 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
49
charger/vehicle-api_test.go
Normal file
49
charger/vehicle-api_test.go
Normal 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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue