From 01e095621a979895a1f405a56e8f73a6fcae8a4a Mon Sep 17 00:00:00 2001 From: Hofy <68365998+Hofyyy@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:36:39 +0200 Subject: [PATCH] Teslamate: add gps position (#28626) --- cmd/decorate/decorate.go | 1 + templates/definition/vehicle/teslamate.yaml | 9 ++++++ vehicle/homeassistant.go | 1 + vehicle/vehicle.go | 36 +++++++++++++++++++-- vehicle/vehicle_decorators.go | 14 +++++++- vehicle/vehicle_decorators_test.go | 34 ++++++++++++++++++- 6 files changed, 91 insertions(+), 4 deletions(-) diff --git a/cmd/decorate/decorate.go b/cmd/decorate/decorate.go index 1c42a6b91..c9955e310 100644 --- a/cmd/decorate/decorate.go +++ b/cmd/decorate/decorate.go @@ -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](), diff --git a/templates/definition/vehicle/teslamate.yaml b/templates/definition/vehicle/teslamate.yaml index 022cb00e0..66e5e1216 100644 --- a/templates/definition/vehicle/teslamate.yaml +++ b/templates/definition/vehicle/teslamate.yaml @@ -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 diff --git a/vehicle/homeassistant.go b/vehicle/homeassistant.go index c0c7df23e..af16614bd 100644 --- a/vehicle/homeassistant.go +++ b/vehicle/homeassistant.go @@ -129,6 +129,7 @@ func NewHomeAssistantVehicleFromConfig(other map[string]any) (api.Vehicle, error wakeup, enable, nil, + nil, ), nil } diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go index 62b2bfa8b..37f80a4f2 100644 --- a/vehicle/vehicle.go +++ b/vehicle/vehicle.go @@ -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 diff --git a/vehicle/vehicle_decorators.go b/vehicle/vehicle_decorators.go index 46a7c0426..3949aaecb 100644 --- a/vehicle/vehicle_decorators.go +++ b/vehicle/vehicle_decorators.go @@ -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) } diff --git a/vehicle/vehicle_decorators_test.go b/vehicle/vehicle_decorators_test.go index b262675a6..9c3a9b535 100644 --- a/vehicle/vehicle_decorators_test.go +++ b/vehicle/vehicle_decorators_test.go @@ -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")