Teslamate: add gps position (#28626)

This commit is contained in:
Hofy 2026-03-30 07:36:39 +02:00 • committed by GitHub
parent c4f6e56fc8
commit 01e095621a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 4 deletions

View file

@ -70,6 +70,7 @@ func init() {
reflect.TypeFor[api.VehicleRange](),
reflect.TypeFor[api.VehicleClimater](),
reflect.TypeFor[api.VehicleFinishTimer](),
reflect.TypeFor[api.VehiclePosition](),
reflect.TypeFor[api.Identifier](),
reflect.TypeFor[api.ChargerEx](),
reflect.TypeFor[api.ChargeRater](),

View file

@ -42,6 +42,15 @@ render: |
source: mqtt
topic: teslamate/cars/{{ .id }}/odometer
timeout: {{ .timeout }}
position:
latitude:
source: mqtt
topic: teslamate/cars/{{ .id }}/latitude
timeout: {{ .timeout }}
longitude:
source: mqtt
topic: teslamate/cars/{{ .id }}/longitude
timeout: {{ .timeout }}
limitsoc:
source: mqtt
topic: teslamate/cars/{{ .id }}/charge_limit_soc

View file

@ -129,6 +129,7 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error
wakeup,
enable,
nil,
nil,
), nil
}

View file

@ -11,7 +11,7 @@ import (
"github.com/evcc-io/evcc/util"
)
//go:generate go tool decorate -f decorateVehicle -b api.Vehicle -t api.SocLimiter,api.ChargeState,api.VehicleRange,api.VehicleOdometer,api.VehicleClimater,api.CurrentController,api.CurrentGetter,api.VehicleFinishTimer,api.Resurrector,api.ChargeController,api.ChargeRater
//go:generate go tool decorate -f decorateVehicle -b api.Vehicle -t api.SocLimiter,api.ChargeState,api.VehicleRange,api.VehicleOdometer,api.VehicleClimater,api.CurrentController,api.CurrentGetter,api.VehicleFinishTimer,api.Resurrector,api.ChargeController,api.ChargeRater,api.VehiclePosition
// Vehicle is an api.Vehicle implementation with configurable getters and setters.
type Vehicle struct {
@ -39,6 +39,10 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.V
Wakeup *plugin.Config
ChargeEnable *plugin.Config
ChargedEnergy *plugin.Config
Position *struct {
Latitude plugin.Config `mapstructure:"latitude"`
Longitude plugin.Config `mapstructure:"longitude"`
} `mapstructure:"position"`
}
if err := util.DecodeOther(other, &cc); err != nil {
@ -107,6 +111,34 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.V
return nil, fmt.Errorf("getMaxCurrent: %w", err)
}
// decorate position
var position func() (float64, float64, error)
if cc.Position != nil {
latG, err := cc.Position.Latitude.FloatGetter(ctx)
if err != nil {
return nil, fmt.Errorf("latitude: %w", err)
}
lonG, err := cc.Position.Longitude.FloatGetter(ctx)
if err != nil {
return nil, fmt.Errorf("longitude: %w", err)
}
position = func() (float64, float64, error) {
lat, err := latG()
if err != nil {
return 0, 0, err
}
lon, err := lonG()
if err != nil {
return 0, 0, err
}
// MQTT sources may report (0,0) when no GPS fix is available
if lat == 0 && lon == 0 {
return 0, 0, api.ErrNotAvailable
}
return lat, lon, nil
}
}
// decorate finishtime
var finishTime func() (time.Time, error)
if cc.FinishTime != nil {
@ -160,7 +192,7 @@ func NewConfigurableFromConfig(ctx context.Context, other map[string]any) (api.V
return nil, errors.New("cannot have charge control without status")
}
return decorateVehicle(v, limitSoc, status, rng, odo, climater, maxCurrent, getMaxCurrent, finishTime, wakeup, chargeEnable, chargedEnergy), nil
return decorateVehicle(v, limitSoc, status, rng, odo, climater, maxCurrent, getMaxCurrent, finishTime, wakeup, chargeEnable, chargedEnergy, position), nil
}
// Soc implements the api.Vehicle interface

View file

@ -9,7 +9,7 @@ import (
"github.com/evcc-io/evcc/api"
)
func decorateVehicle(base api.Vehicle, socLimiter func() (int64, error), chargeState func() (api.ChargeStatus, error), vehicleRange func() (int64, error), vehicleOdometer func() (float64, error), vehicleClimater func() (bool, error), currentController func(int64) error, currentGetter func() (float64, error), vehicleFinishTimer func() (time.Time, error), resurrector func() error, chargeController func(bool) error, chargeRater func() (float64, error)) api.Vehicle {
func decorateVehicle(base api.Vehicle, socLimiter func() (int64, error), chargeState func() (api.ChargeStatus, error), vehicleRange func() (int64, error), vehicleOdometer func() (float64, error), vehicleClimater func() (bool, error), currentController func(int64) error, currentGetter func() (float64, error), vehicleFinishTimer func() (time.Time, error), resurrector func() error, chargeController func(bool) error, chargeRater func() (float64, error), vehiclePosition func() (float64, float64, error)) api.Vehicle {
caps := make(map[reflect.Type]any)
if socLimiter != nil {
@ -56,6 +56,10 @@ func decorateVehicle(base api.Vehicle, socLimiter func() (int64, error), chargeS
caps[reflect.TypeFor[api.ChargeRater]()] = &decorateVehicleChargeRaterImpl{chargeRater: chargeRater}
}
if vehiclePosition != nil {
caps[reflect.TypeFor[api.VehiclePosition]()] = &decorateVehicleVehiclePositionImpl{vehiclePosition: vehiclePosition}
}
if len(caps) == 0 {
return base
}
@ -153,6 +157,14 @@ func (impl *decorateVehicleVehicleOdometerImpl) Odometer() (float64, error) {
return impl.vehicleOdometer()
}
type decorateVehicleVehiclePositionImpl struct {
vehiclePosition func() (float64, float64, error)
}
func (impl *decorateVehicleVehiclePositionImpl) Position() (float64, float64, error) {
return impl.vehiclePosition()
}
type decorateVehicleVehicleRangeImpl struct {
vehicleRange func() (int64, error)
}

View file

@ -17,7 +17,7 @@ func baseVehicle() *Vehicle {
}
func TestDecorateVehicle_NoCapabilities(t *testing.T) {
v := decorateVehicle(baseVehicle(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
v := decorateVehicle(baseVehicle(), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
// base vehicle interface works
soc, err := v.Soc()
@ -43,6 +43,7 @@ func TestDecorateVehicle_AllCapabilities(t *testing.T) {
func() error { return nil }, // resurrector
func(bool) error { return nil }, // chargeController
nil, // chargeRater
func() (float64, float64, error) { return 50.1234, 8.5678, nil }, // vehiclePosition
)
// base interface
@ -109,12 +110,42 @@ func TestDecorateVehicle_AllCapabilities(t *testing.T) {
require.True(t, ok, "Resurrector")
assert.NoError(t, r.WakeUp())
// VehiclePosition
vp, ok := api.Cap[api.VehiclePosition](v)
require.True(t, ok, "VehiclePosition")
lat, lon, err := vp.Position()
assert.NoError(t, err)
assert.Equal(t, 50.1234, lat)
assert.Equal(t, 8.5678, lon)
// ChargeController
chc, ok := api.Cap[api.ChargeController](v)
require.True(t, ok, "ChargeController")
assert.NoError(t, chc.ChargeEnable(true))
}
func TestDecorateVehicle_PositionNotAvailable(t *testing.T) {
v := decorateVehicle(baseVehicle(),
nil, // socLimiter
nil, // chargeState
nil, // vehicleRange
nil, // vehicleOdometer
nil, // vehicleClimater
nil, // currentController
nil, // currentGetter
nil, // vehicleFinishTimer
nil, // resurrector
nil, // chargeController
nil, // chargeRater
func() (float64, float64, error) { return 0, 0, api.ErrNotAvailable }, // vehiclePosition
)
vp, ok := api.Cap[api.VehiclePosition](v)
require.True(t, ok, "VehiclePosition")
_, _, err := vp.Position()
assert.ErrorIs(t, err, api.ErrNotAvailable)
}
func TestDecorateVehicle_PartialCapabilities(t *testing.T) {
v := decorateVehicle(baseVehicle(),
func() (int64, error) { return 80, nil }, // socLimiter
@ -128,6 +159,7 @@ func TestDecorateVehicle_PartialCapabilities(t *testing.T) {
func() error { return nil }, // resurrector
nil, // chargeController
nil, // chargeRater
nil, // vehiclePosition
)
assert.True(t, api.HasCap[api.SocLimiter](v), "SocLimiter should be present")