diff --git a/api/api.go b/api/api.go index dedfcb334..a0b0e8728 100644 --- a/api/api.go +++ b/api/api.go @@ -97,6 +97,11 @@ type VehicleStatus interface { Status() (ChargeStatus, error) } +// VehicleRange provides the vehicles remaining km range +type VehicleRange interface { + Range() (int64, error) +} + // Climater provides climatisation data type Climater interface { Climater() (active bool, outsideTemp float64, targetTemp float64, err error) diff --git a/cmd/dumper.go b/cmd/dumper.go index 392714d63..381473c4b 100644 --- a/cmd/dumper.go +++ b/cmd/dumper.go @@ -74,12 +74,6 @@ func (d *dumper) Dump(name string, v interface{}) { // charger if v, ok := v.(api.Charger); ok { - // if status, err := v.Status(); err != nil { - // fmt.Fprintf(w, "Status:\t%v\n", err) - // } else { - // fmt.Fprintf(w, "Status:\t%s\n", status) - // } - if enabled, err := v.Enabled(); err != nil { fmt.Fprintf(w, "Enabled:\t%v\n", err) } else { @@ -103,23 +97,23 @@ func (d *dumper) Dump(name string, v interface{}) { } } - if v, ok := v.(api.ChargeFinishTimer); ok { - if ft, err := v.FinishTime(); err != nil { - fmt.Fprintf(w, "Finish time:\t%v\n", err) - } else { - fmt.Fprintf(w, "Finish time:\t%v\n", ft.Truncate(time.Minute)) - } - } - // vehicle if v, ok := v.(api.Vehicle); ok { - fmt.Printf("Capacity: %dkWh\n", v.Capacity()) + fmt.Fprintf(w, "Capacity:\t%dkWh\n", v.Capacity()) if soc, err := v.ChargeState(); err != nil { - fmt.Printf("State: %v\n", err) + fmt.Fprintf(w, "State:\t%v\n", err) } else { - fmt.Printf("State: %.0f%%\n", soc) + fmt.Fprintf(w, "State:\t%.0f%%\n", soc) + } + } + + if v, ok := v.(api.VehicleRange); ok { + if rng, err := v.Range(); err != nil { + fmt.Fprintf(w, "Vehicle range:\t%v\n", err) + } else { + fmt.Fprintf(w, "Vehicle range:\t%vkm\n", rng) } } @@ -131,6 +125,14 @@ func (d *dumper) Dump(name string, v interface{}) { } } + if v, ok := v.(api.ChargeFinishTimer); ok { + if ft, err := v.FinishTime(); err != nil { + fmt.Fprintf(w, "Finish time:\t%v\n", err) + } else { + fmt.Fprintf(w, "Finish time:\t%v\n", ft.Truncate(time.Minute)) + } + } + if v, ok := v.(api.Climater); ok { if active, ot, tt, err := v.Climater(); err != nil { fmt.Fprintf(w, "Climater:\t%v\n", err) diff --git a/core/loadpoint.go b/core/loadpoint.go index 85d4fb58f..919b76934 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -750,6 +750,20 @@ func (lp *LoadPoint) publishSoC() { lp.publish("chargeEstimate", time.Duration(-1)) } +// publish remaining vehicle range +func (lp *LoadPoint) publishRange() { + if vs, ok := lp.vehicle.(api.VehicleRange); ok { + if rng, err := vs.Range(); err == nil { + lp.log.DEBUG.Printf("vehicle range: %vkm", rng) + lp.publish("range", rng) + + return + } + } + + lp.publish("range", -1) +} + // Update is the main control function. It reevaluates meters and charger state func (lp *LoadPoint) Update(sitePower float64) { mode := lp.GetMode() @@ -779,6 +793,7 @@ func (lp *LoadPoint) Update(sitePower float64) { // initial update of connected state matches charger status lp.findActiveVehicle() lp.publishSoC() + lp.publishRange() // sync settings with charger lp.syncCharger() diff --git a/vehicle/kamereon/api.go b/vehicle/kamereon/api.go index abe770d01..1bff4a722 100644 --- a/vehicle/kamereon/api.go +++ b/vehicle/kamereon/api.go @@ -6,6 +6,7 @@ import ( "github.com/andig/evcc/api" ) +// Response structure for kamereon api type Response struct { Data data `json:"data"` } @@ -18,7 +19,8 @@ type attributes struct { Timestamp string `json:"timestamp"` ChargingStatus float32 `json:"chargingStatus"` InstantaneousPower int `json:"instantaneousPower"` - RangeHvacOff int `json:"rangeHvacOff"` + RangeHvacOff int `json:"rangeHvacOff"` // Nissan + BatteryAutonomy int `json:"batteryAutonomy"` // Renault BatteryLevel int `json:"batteryLevel"` BatteryCapacity int `json:"batteryCapacity"` // Nissan BatteryTemperature int `json:"batteryTemperature"` @@ -66,6 +68,17 @@ func (v *API) Status() (api.ChargeStatus, error) { return status, err } +// Range implements the Vehicle.Range interface +func (v *API) Range() (int64, error) { + res, err := v.apiG() + + if res, ok := res.(Response); err == nil && ok { + return int64(res.Data.Attributes.RangeHvacOff), nil + } + + return 0, err +} + // FinishTime implements the Vehicle.ChargeFinishTimer interface func (v *API) FinishTime() (time.Time, error) { res, err := v.apiG() diff --git a/vehicle/renault.go b/vehicle/renault.go index 46eac2919..b76c0f614 100644 --- a/vehicle/renault.go +++ b/vehicle/renault.go @@ -79,6 +79,7 @@ type attributes struct { ChargingStatus float32 `json:"chargingStatus"` InstantaneousPower int `json:"instantaneousPower"` RangeHvacOff int `json:"rangeHvacOff"` + BatteryAutonomy int `json:"batteryAutonomy"` BatteryLevel int `json:"batteryLevel"` BatteryTemperature int `json:"batteryTemperature"` PlugStatus int `json:"plugStatus"` @@ -341,6 +342,17 @@ func (v *Renault) Status() (api.ChargeStatus, error) { return status, err } +// Range implements the Vehicle.Range interface +func (v *Renault) Range() (int64, error) { + res, err := v.apiG() + + if res, ok := res.(kamereonResponse); err == nil && ok { + return int64(res.Data.Attributes.BatteryAutonomy), nil + } + + return 0, err +} + // FinishTime implements the Vehicle.ChargeFinishTimer interface func (v *Renault) FinishTime() (time.Time, error) { res, err := v.apiG() diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go index 42e1a25f2..dc110db66 100644 --- a/vehicle/vehicle.go +++ b/vehicle/vehicle.go @@ -24,13 +24,14 @@ func (m *embed) Capacity() int64 { return m.capacity } -//go:generate go run ../cmd/tools/decorate.go -p vehicle -f decorateVehicle -b api.Vehicle -o vehicle_decorators -t "api.VehicleStatus,Status,func() (api.ChargeStatus, error)" +//go:generate go run ../cmd/tools/decorate.go -p vehicle -f decorateVehicle -b api.Vehicle -o vehicle_decorators -t "api.VehicleStatus,Status,func() (api.ChargeStatus, error)" -t "api.VehicleRange,Range,func() (int64, error)" // Vehicle is an api.Vehicle implementation with configurable getters and setters. type Vehicle struct { *embed chargeG func() (float64, error) statusG func() (string, error) + rangeG func() (int64, error) } func init() { @@ -44,6 +45,7 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error Capacity int64 Charge provider.Config Status *provider.Config + Range *provider.Config Cache time.Duration }{} @@ -81,7 +83,17 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error status = v.status } - res := decorateVehicle(v, status) + // decorate vehicle with Range + var rng func() (int64, error) + if cc.Range != nil { + v.rangeG, err = provider.NewIntGetterFromConfig(*cc.Range) + if err != nil { + return nil, err + } + rng = v.rng + } + + res := decorateVehicle(v, status, rng) return res, nil } @@ -102,3 +114,8 @@ func (m *Vehicle) status() (api.ChargeStatus, error) { return status, err } + +// rng implements the Vehicle.Range interface +func (m *Vehicle) rng() (int64, error) { + return m.rangeG() +} diff --git a/vehicle/vehicle_decorators.go b/vehicle/vehicle_decorators.go index 71de8f871..ae01c369b 100644 --- a/vehicle/vehicle_decorators.go +++ b/vehicle/vehicle_decorators.go @@ -6,12 +6,12 @@ import ( "github.com/andig/evcc/api" ) -func decorateVehicle(base api.Vehicle, vehicleStatus func() (api.ChargeStatus, error)) api.Vehicle { +func decorateVehicle(base api.Vehicle, vehicleStatus func() (api.ChargeStatus, error), vehicleRange func() (int64, error)) api.Vehicle { switch { - case vehicleStatus == nil: + case vehicleRange == nil && vehicleStatus == nil: return base - case vehicleStatus != nil: + case vehicleRange == nil && vehicleStatus != nil: return &struct { api.Vehicle api.VehicleStatus @@ -21,11 +21,45 @@ func decorateVehicle(base api.Vehicle, vehicleStatus func() (api.ChargeStatus, e vehicleStatus: vehicleStatus, }, } + + case vehicleRange != nil && vehicleStatus == nil: + return &struct { + api.Vehicle + api.VehicleRange + }{ + Vehicle: base, + VehicleRange: &decorateVehicleVehicleRangeImpl{ + vehicleRange: vehicleRange, + }, + } + + case vehicleRange != nil && vehicleStatus != nil: + return &struct { + api.Vehicle + api.VehicleRange + api.VehicleStatus + }{ + Vehicle: base, + VehicleRange: &decorateVehicleVehicleRangeImpl{ + vehicleRange: vehicleRange, + }, + VehicleStatus: &decorateVehicleVehicleStatusImpl{ + vehicleStatus: vehicleStatus, + }, + } } return nil } +type decorateVehicleVehicleRangeImpl struct { + vehicleRange func() (int64, error) +} + +func (impl *decorateVehicleVehicleRangeImpl) Range() (int64, error) { + return impl.vehicleRange() +} + type decorateVehicleVehicleStatusImpl struct { vehicleStatus func() (api.ChargeStatus, error) } diff --git a/vehicle/vw/api.go b/vehicle/vw/api.go index 6ff1ccb3f..3b232a537 100644 --- a/vehicle/vw/api.go +++ b/vehicle/vw/api.go @@ -51,6 +51,13 @@ type ChargerResponse struct { PlugStatusData struct { PlugState TimedString // connected } + CruisingRangeStatusData struct { + EngineTypeFirstEngine TimedString // typeIsElectric, petrolGasoline + EngineTypeSecondEngine TimedString // typeIsElectric, petrolGasoline + PrimaryEngineRange TimedInt + SecondaryEngineRange TimedInt + HybridRange TimedInt + } } } } diff --git a/vehicle/vw/impl.go b/vehicle/vw/impl.go index 6f48ada08..2775e2360 100644 --- a/vehicle/vw/impl.go +++ b/vehicle/vw/impl.go @@ -74,6 +74,21 @@ func (v *Implementation) Status() (api.ChargeStatus, error) { return status, err } +// Range implements the Vehicle.Range interface +func (v *Implementation) Range() (rng int64, err error) { + res, err := v.chargerG() + if res, ok := res.(ChargerResponse); err == nil && ok { + crsd := res.Charger.Status.CruisingRangeStatusData + + rng = int64(crsd.PrimaryEngineRange.Content) + if crsd.EngineTypeFirstEngine.Content != "typeIsElectric" { + rng = int64(crsd.SecondaryEngineRange.Content) + } + } + + return rng, err +} + // Climater implements the Vehicle.Climater interface func (v *Implementation) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) { res, err := v.climateG()