Always enable charging when climate request active (#381)
This commit is contained in:
parent
8e61200216
commit
43402b3661
8 changed files with 196 additions and 91 deletions
|
|
@ -91,3 +91,8 @@ type Vehicle interface {
|
|||
type ChargeFinishTimer interface {
|
||||
FinishTime() (time.Time, error)
|
||||
}
|
||||
|
||||
// Climater provides climatisation data
|
||||
type Climater interface {
|
||||
Climater() (active bool, outsideTemp float64, targetTemp float64, err error)
|
||||
}
|
||||
|
|
|
|||
15
cmd/dump.go
15
cmd/dump.go
|
|
@ -2,6 +2,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
|
|
@ -66,6 +67,20 @@ func dumpAPIs(v interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
if v, ok := v.(api.Climater); ok {
|
||||
if active, ot, tt, err := v.Climater(); err != nil {
|
||||
fmt.Printf("Climater: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Climate active: %v\n", active)
|
||||
if !math.IsNaN(ot) {
|
||||
fmt.Printf("Outside temp: %.1f°C\n", ot)
|
||||
}
|
||||
if !math.IsNaN(tt) {
|
||||
fmt.Printf("Target temp: %.1f°C\n", tt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := v.(api.Diagnosis); ok {
|
||||
fmt.Println("Diagnostic dump:")
|
||||
v.Diagnosis()
|
||||
|
|
|
|||
|
|
@ -382,6 +382,21 @@ func (lp *LoadPoint) targetSocReached(socCharge, targetSoC float64) bool {
|
|||
return targetSoC > 0 && targetSoC < 100 && socCharge >= targetSoC
|
||||
}
|
||||
|
||||
// climateActive checks if vehicle has active climate request
|
||||
func (lp *LoadPoint) climateActive() bool {
|
||||
if cl, ok := lp.vehicle.(api.Climater); ok {
|
||||
active, outsideTemp, targetTemp, err := cl.Climater()
|
||||
if err == nil {
|
||||
lp.log.DEBUG.Printf("climater active: %v, target temp: %.1f°C, outside temp: %.1f°C", active, targetTemp, outsideTemp)
|
||||
return active
|
||||
}
|
||||
|
||||
lp.log.ERROR.Printf("climater: %v", err)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// updateChargerStatus updates car status and detects car connected/disconnected events
|
||||
func (lp *LoadPoint) updateChargerStatus() error {
|
||||
status, err := lp.handler.Status()
|
||||
|
|
@ -656,7 +671,11 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
err = lp.handler.Ramp(0)
|
||||
|
||||
case lp.targetSocReached(lp.socCharge, float64(lp.TargetSoC)):
|
||||
err = lp.handler.Ramp(0)
|
||||
var targetCurrent int64
|
||||
if lp.climateActive() {
|
||||
targetCurrent = lp.MinCurrent
|
||||
}
|
||||
err = lp.handler.Ramp(targetCurrent, true)
|
||||
|
||||
case mode == api.ModeOff:
|
||||
err = lp.handler.Ramp(0, true)
|
||||
|
|
@ -668,7 +687,13 @@ func (lp *LoadPoint) Update(sitePower float64) {
|
|||
targetCurrent := lp.maxCurrent(mode, sitePower)
|
||||
lp.log.DEBUG.Printf("target charge current: %dA", targetCurrent)
|
||||
|
||||
err = lp.handler.Ramp(targetCurrent)
|
||||
var required bool // false
|
||||
if targetCurrent == 0 && lp.climateActive() {
|
||||
targetCurrent = lp.MinCurrent
|
||||
required = true
|
||||
}
|
||||
|
||||
err = lp.handler.Ramp(targetCurrent, required)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -111,10 +111,14 @@ func TestUpdate(t *testing.T) {
|
|||
h.EXPECT().Ramp(lpMaxCurrent, true)
|
||||
}},
|
||||
{api.StatusB, api.ModeMinPV, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(lpMinCurrent) // min since update called with 0
|
||||
// min since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(lpMinCurrent, false)
|
||||
}},
|
||||
{api.StatusB, api.ModePV, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0)) // zero since update called with 0
|
||||
// zero since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
// h.EXPECT().Enabled().Return(false) // short-circuited due to status != C
|
||||
}},
|
||||
|
||||
|
|
@ -125,10 +129,14 @@ func TestUpdate(t *testing.T) {
|
|||
h.EXPECT().Ramp(lpMaxCurrent, true)
|
||||
}},
|
||||
{api.StatusC, api.ModeMinPV, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(lpMinCurrent) // min since update called with 0
|
||||
// min since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(lpMinCurrent, false)
|
||||
}},
|
||||
{api.StatusC, api.ModePV, func(h *mock.MockHandler) {
|
||||
h.EXPECT().Ramp(int64(0)) // zero since update called with 0
|
||||
// zero since update called with 0
|
||||
// force = false due to pv mode climater check
|
||||
h.EXPECT().Ramp(int64(0), false)
|
||||
h.EXPECT().Enabled().Return(false)
|
||||
}},
|
||||
}
|
||||
|
|
@ -412,7 +420,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
|||
handler.EXPECT().Status().Return(api.StatusC, nil)
|
||||
vehicle.EXPECT().ChargeState().Return(90.0, nil)
|
||||
handler.EXPECT().SyncEnabled().Return()
|
||||
handler.EXPECT().Ramp(int64(0)).Return(nil)
|
||||
handler.EXPECT().Ramp(int64(0), true).Return(nil) // true due to immediately handling climate requests
|
||||
lp.Update(500)
|
||||
|
||||
// deactivated charger changes status to B
|
||||
|
|
@ -422,7 +430,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
|
|||
handler.EXPECT().TargetCurrent().Return(int64(0)) // once more for status changes
|
||||
vehicle.EXPECT().ChargeState().Return(95.0, nil)
|
||||
handler.EXPECT().SyncEnabled().Return()
|
||||
handler.EXPECT().Ramp(int64(0)).Return(nil)
|
||||
handler.EXPECT().Ramp(int64(0), true).Return(nil) // true due to immediately handling climate requests
|
||||
lp.Update(-5000)
|
||||
|
||||
// soc has fallen below target
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/request"
|
||||
"github.com/andig/evcc/vehicle/vw"
|
||||
|
|
@ -22,10 +21,10 @@ import (
|
|||
type Audi struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
user, password string
|
||||
tokens vw.Tokens
|
||||
api *vw.API
|
||||
apiG func() (interface{}, error)
|
||||
user, password string
|
||||
tokens vw.Tokens
|
||||
api *vw.API
|
||||
*vw.Implementation // provides the api implementations
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -56,7 +55,7 @@ func NewAudiFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
v.api = vw.NewAPI(v.Helper, &v.tokens, v.authFlow, v.refreshHeaders, strings.ToUpper(cc.VIN), "Audi", "DE")
|
||||
v.apiG = provider.NewCached(v.apiCall, cc.Cache).InterfaceGetter()
|
||||
v.Implementation = vw.NewImplementation(v.api, cc.Cache)
|
||||
|
||||
var err error
|
||||
jar, err := cookiejar.New(&cookiejar.Options{
|
||||
|
|
@ -162,34 +161,3 @@ func (v *Audi) refreshHeaders() map[string]string {
|
|||
"X-Client-Id": audiClientID,
|
||||
}
|
||||
}
|
||||
|
||||
// apiCall provides charger api response
|
||||
func (v *Audi) apiCall() (interface{}, error) {
|
||||
res, err := v.api.Charger()
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Audi) ChargeState() (float64, error) {
|
||||
res, err := v.apiG()
|
||||
if res, ok := res.(vw.ChargerResponse); err == nil && ok {
|
||||
return float64(res.Charger.Status.BatteryStatusData.StateOfCharge.Content), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// FinishTime implements the Vehicle.ChargeFinishTimer interface
|
||||
func (v *Audi) FinishTime() (time.Time, error) {
|
||||
res, err := v.apiG()
|
||||
if res, ok := res.(vw.ChargerResponse); err == nil && ok {
|
||||
var timestamp time.Time
|
||||
if err == nil {
|
||||
timestamp, err = time.Parse(time.RFC3339, res.Charger.Status.BatteryStatusData.RemainingChargingTime.Timestamp)
|
||||
}
|
||||
|
||||
return timestamp.Add(time.Duration(res.Charger.Status.BatteryStatusData.RemainingChargingTime.Content) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/request"
|
||||
"github.com/andig/evcc/vehicle/vw"
|
||||
|
|
@ -22,11 +21,11 @@ import (
|
|||
type VW struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
user, password string
|
||||
clientID string
|
||||
tokens vw.Tokens
|
||||
api *vw.API
|
||||
apiG func() (interface{}, error)
|
||||
user, password string
|
||||
clientID string
|
||||
tokens vw.Tokens
|
||||
api *vw.API
|
||||
*vw.Implementation // provides the api implementations
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -55,7 +54,7 @@ func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
v.api = vw.NewAPI(v.Helper, &v.tokens, v.authFlow, v.refreshHeaders, strings.ToUpper(cc.VIN), "VW", "DE")
|
||||
v.apiG = provider.NewCached(v.apiCall, cc.Cache).InterfaceGetter()
|
||||
v.Implementation = vw.NewImplementation(v.api, cc.Cache)
|
||||
|
||||
var err error
|
||||
jar, err := cookiejar.New(&cookiejar.Options{
|
||||
|
|
@ -200,34 +199,3 @@ func (v *VW) refreshHeaders() map[string]string {
|
|||
"X-Client-Id": v.clientID,
|
||||
}
|
||||
}
|
||||
|
||||
// apiCall provides charger api response
|
||||
func (v *VW) apiCall() (interface{}, error) {
|
||||
res, err := v.api.Charger()
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
func (v *VW) ChargeState() (float64, error) {
|
||||
res, err := v.apiG()
|
||||
if res, ok := res.(vw.ChargerResponse); err == nil && ok {
|
||||
return float64(res.Charger.Status.BatteryStatusData.StateOfCharge.Content), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// FinishTime implements the Vehicle.ChargeFinishTimer interface
|
||||
func (v *VW) FinishTime() (time.Time, error) {
|
||||
res, err := v.apiG()
|
||||
if res, ok := res.(vw.ChargerResponse); err == nil && ok {
|
||||
var timestamp time.Time
|
||||
if err == nil {
|
||||
timestamp, err = time.Parse(time.RFC3339, res.Charger.Status.BatteryStatusData.RemainingChargingTime.Timestamp)
|
||||
}
|
||||
|
||||
return timestamp.Add(time.Duration(res.Charger.Status.BatteryStatusData.RemainingChargingTime.Content) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,24 +20,58 @@ type VehiclesResponse struct {
|
|||
}
|
||||
}
|
||||
|
||||
// TimedInt is an int value with timestamp
|
||||
type TimedInt struct {
|
||||
Content int
|
||||
Timestamp string
|
||||
}
|
||||
|
||||
// TimedString is a string value with timestamp
|
||||
type TimedString struct {
|
||||
Content string
|
||||
Timestamp string
|
||||
}
|
||||
|
||||
// ChargerResponse is the /bs/batterycharge/v1/%s/%s/vehicles/%s/charger api
|
||||
type ChargerResponse struct {
|
||||
Charger struct {
|
||||
Status struct {
|
||||
BatteryStatusData struct {
|
||||
StateOfCharge struct {
|
||||
Content int
|
||||
Timestamp string
|
||||
}
|
||||
RemainingChargingTime struct {
|
||||
Content int
|
||||
Timestamp string
|
||||
}
|
||||
StateOfCharge TimedInt
|
||||
RemainingChargingTime TimedInt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ClimaterResponse is the /bs/climatisation/v1/%s/%s/vehicles/%s/climater api
|
||||
type ClimaterResponse struct {
|
||||
Climater struct {
|
||||
Settings struct {
|
||||
TargetTemperature TimedInt
|
||||
HeaterSource TimedString
|
||||
}
|
||||
Status struct {
|
||||
ClimatisationStatusData struct {
|
||||
ClimatisationState TimedString
|
||||
ClimatisationReason TimedString
|
||||
RemainingClimatisationTime TimedInt
|
||||
}
|
||||
TemperatureStatusData struct {
|
||||
OutdoorTemperature TimedInt
|
||||
}
|
||||
VehicleParkingClockStatusData struct {
|
||||
VehicleParkingClock TimedString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temp2Float converts api temp to float value
|
||||
func Temp2Float(val int) float64 {
|
||||
return float64(val)/10 - 273
|
||||
}
|
||||
|
||||
// API is the VW api client
|
||||
type API struct {
|
||||
*request.Helper
|
||||
|
|
@ -140,3 +174,19 @@ func (v *API) Charger() (ChargerResponse, error) {
|
|||
err := v.getJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Climater implements the /climater response
|
||||
func (v *API) Climater() (ClimaterResponse, error) {
|
||||
var res ClimaterResponse
|
||||
uri := fmt.Sprintf("%s/bs/climatisation/v1/%s/%s/vehicles/%s/climater", BaseURI, v.brand, v.country, v.VIN)
|
||||
err := v.getJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Any implements any api response
|
||||
func (v *API) Any(base string) (interface{}, error) {
|
||||
var res interface{}
|
||||
uri := fmt.Sprintf("%s/"+strings.TrimLeft(base, "/"), BaseURI, v.brand, v.country, v.VIN)
|
||||
err := v.getJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
|
|
|||
66
vehicle/vw/impl.go
Normal file
66
vehicle/vw/impl.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package vw
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/provider"
|
||||
)
|
||||
|
||||
// Implementation implements the evcc vehicle api
|
||||
type Implementation struct {
|
||||
chargerG func() (interface{}, error)
|
||||
climateG func() (interface{}, error)
|
||||
}
|
||||
|
||||
// NewImplementation provides the evcc vehicle api implementation
|
||||
func NewImplementation(api *API, cache time.Duration) *Implementation {
|
||||
impl := &Implementation{
|
||||
chargerG: provider.NewCached(func() (interface{}, error) {
|
||||
return api.Charger()
|
||||
}, cache).InterfaceGetter(),
|
||||
climateG: provider.NewCached(func() (interface{}, error) {
|
||||
return api.Climater()
|
||||
}, cache).InterfaceGetter(),
|
||||
}
|
||||
return impl
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Implementation) ChargeState() (float64, error) {
|
||||
res, err := v.chargerG()
|
||||
if res, ok := res.(ChargerResponse); err == nil && ok {
|
||||
return float64(res.Charger.Status.BatteryStatusData.StateOfCharge.Content), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// FinishTime implements the Vehicle.ChargeFinishTimer interface
|
||||
func (v *Implementation) FinishTime() (time.Time, error) {
|
||||
res, err := v.chargerG()
|
||||
if res, ok := res.(ChargerResponse); err == nil && ok {
|
||||
var timestamp time.Time
|
||||
if err == nil {
|
||||
timestamp, err = time.Parse(time.RFC3339, res.Charger.Status.BatteryStatusData.RemainingChargingTime.Timestamp)
|
||||
}
|
||||
|
||||
return timestamp.Add(time.Duration(res.Charger.Status.BatteryStatusData.RemainingChargingTime.Content) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
// Climater implements the Vehicle.Climater interface
|
||||
func (v *Implementation) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
|
||||
res, err := v.climateG()
|
||||
if res, ok := res.(ClimaterResponse); err == nil && ok {
|
||||
active = "off" != strings.ToLower(res.Climater.Status.ClimatisationStatusData.ClimatisationState.Content)
|
||||
outsideTemp = Temp2Float(res.Climater.Status.TemperatureStatusData.OutdoorTemperature.Content)
|
||||
targetTemp = Temp2Float(res.Climater.Settings.TargetTemperature.Content)
|
||||
|
||||
return active, outsideTemp, targetTemp, nil
|
||||
}
|
||||
|
||||
return active, outsideTemp, targetTemp, err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue