diff --git a/api/api.go b/api/api.go index c5f441711..224eeab38 100644 --- a/api/api.go +++ b/api/api.go @@ -92,6 +92,11 @@ type ChargeFinishTimer interface { FinishTime() (time.Time, error) } +// VehicleStatus provides the vehicles current charging status +type VehicleStatus interface { + Status() (ChargeStatus, error) +} + // Climater provides climatisation data type Climater interface { Climater() (active bool, outsideTemp float64, targetTemp float64, err error) diff --git a/cmd/dump.go b/cmd/dump.go index 29e1fae6f..89e87eb1b 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -51,6 +51,14 @@ func dumpAPIs(v interface{}) { } } + if v, ok := v.(api.VehicleStatus); ok { + if status, err := v.Status(); err != nil { + fmt.Printf("Charge status: %v\n", err) + } else { + fmt.Printf("Charge status: %v\n", status) + } + } + if v, ok := v.(api.ChargeTimer); ok { if duration, err := v.ChargingTime(); err != nil { fmt.Printf("Duration: %v\n", err) diff --git a/core/loadpoint.go b/core/loadpoint.go index 5d689b539..5bfc51cfc 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -58,11 +58,12 @@ type LoadPoint struct { sync.Mutex // guard status Mode api.ChargeMode `mapstructure:"mode"` // Charge mode, guarded by mutex - Title string `mapstructure:"title"` // UI title - Phases int64 `mapstructure:"phases"` // Phases- required for converting power and current - ChargerRef string `mapstructure:"charger"` // Charger reference - VehicleRef string `mapstructure:"vehicle"` // Vehicle reference - Meters struct { + Title string `mapstructure:"title"` // UI title + Phases int64 `mapstructure:"phases"` // Phases- required for converting power and current + ChargerRef string `mapstructure:"charger"` // Charger reference + VehicleRef string `mapstructure:"vehicle"` // Vehicle reference + VehiclesRef []string `mapstructure:"vehicles"` // Vehicles reference + Meters struct { ChargeMeterRef string `mapstructure:"charge"` // Charge meter reference } SoC SoCConfig @@ -78,8 +79,9 @@ type LoadPoint struct { chargeTimer api.ChargeTimer chargeRater api.ChargeRater - chargeMeter api.Meter // Charger usage meter - vehicle api.Vehicle // Vehicle + chargeMeter api.Meter // Charger usage meter + vehicle api.Vehicle // Currently active vehicle + vehicles []api.Vehicle // Assigned vehicles socEstimator *wrapper.SocEstimator // cached state @@ -120,9 +122,22 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin if lp.Meters.ChargeMeterRef != "" { lp.chargeMeter = cp.Meter(lp.Meters.ChargeMeterRef) } + + // multiple vehicles + for _, ref := range lp.VehiclesRef { + vehicle := cp.Vehicle(ref) + lp.vehicles = append(lp.vehicles, vehicle) + } + + // single vehicle if lp.VehicleRef != "" { - lp.vehicle = cp.Vehicle(lp.VehicleRef) - lp.socEstimator = wrapper.NewSocEstimator(log, lp.vehicle, lp.SoC.Estimate) + vehicle := cp.Vehicle(lp.VehicleRef) + lp.vehicles = append(lp.vehicles, vehicle) + } + + // use first vehicle for estimator + if len(lp.vehicles) > 0 { + lp.setActiveVehicle(lp.vehicles[0]) } if lp.ChargerRef == "" { @@ -222,7 +237,9 @@ func (lp *LoadPoint) notify(event string) { // publish sends values to UI and databases func (lp *LoadPoint) publish(key string, val interface{}) { - lp.uiChan <- util.Param{Key: key, Val: val} + if lp.uiChan != nil { + lp.uiChan <- util.Param{Key: key, Val: val} + } } // evChargeStartHandler sends external start event @@ -385,6 +402,59 @@ func (lp *LoadPoint) climateActive() bool { return false } +// setActiveVehicle assigns currently active vehicle and configures soc estimator +func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { + if lp.vehicle != nil { + lp.log.INFO.Printf("vehicle updated: %s -> %s", lp.vehicle.Title(), vehicle.Title()) + } + + lp.vehicle = vehicle + lp.socEstimator = wrapper.NewSocEstimator(lp.log, vehicle, lp.SoC.Estimate) + + lp.publish("socCapacity", lp.vehicle.Capacity()) + lp.publish("socTitle", lp.vehicle.Title()) +} + +// findActiveVehicle validates if the active vehicle is still connected to the loadpoint +func (lp *LoadPoint) findActiveVehicle() { + if len(lp.vehicles) <= 1 { + return + } + + if vs, ok := lp.vehicle.(api.VehicleStatus); ok { + status, err := vs.Status() + + if err == nil { + lp.log.DEBUG.Printf("vehicle status: %s (%s)", status, lp.vehicle.Title()) + + // vehicle is plugged or charging, so it should be the right one + if status == api.StatusB || status == api.StatusC { + return + } + + for _, vehicle := range lp.vehicles { + if vehicle == lp.vehicle { + continue + } + + if vs, ok := vehicle.(api.VehicleStatus); ok { + status, err := vs.Status() + + if err == nil { + lp.log.DEBUG.Printf("vehicle status: %s (%s)", status, vehicle.Title()) + + // vehicle is plugged or charging, so it should be the right one + if status == api.StatusB || status == api.StatusC { + lp.setActiveVehicle(vehicle) + return + } + } + } + } + } + } +} + // updateChargerStatus updates car status and detects car connected/disconnected events func (lp *LoadPoint) updateChargerStatus() error { status, err := lp.handler.Status() @@ -634,6 +704,9 @@ func (lp *LoadPoint) Update(sitePower float64) { // update progress and soc before status is updated lp.publishChargeProgress() + + // update active vehicle and publish soc + lp.findActiveVehicle() lp.publishSoC() // read and publish status diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 6174e7e83..d4552ec44 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -66,6 +66,9 @@ loadpoints: meters: charge: charge # charge meter vehicle: audi + # vehicles: # use if multiple vehicles allowed to charge on this loadpoint + # - ID.3 + # - e-Up mode: pv soc: min: 0 # immediately charge to 0% regardless of mode unless "off" (disabled) diff --git a/vehicle/renault.go b/vehicle/renault.go index 2b59b96e1..46eac2919 100644 --- a/vehicle/renault.go +++ b/vehicle/renault.go @@ -71,20 +71,20 @@ type kamereonVehicle struct { } type kamereonData struct { - Attributes batteryAttributes `json:"attributes"` + Attributes attributes `json:"attributes"` } -type batteryAttributes struct { - Timestamp string `json:"timestamp"` - ChargeStatus int `json:"chargeStatus"` - InstantaneousPower int `json:"instantaneousPower"` - RangeHvacOff int `json:"rangeHvacOff"` - BatteryLevel int `json:"batteryLevel"` - BatteryTemperature int `json:"batteryTemperature"` - PlugStatus int `json:"plugStatus"` - LastUpdateTime string `json:"lastUpdateTime"` - ChargePower int `json:"chargePower"` - RemainingTime *int `json:"chargingRemainingTime"` +type attributes struct { + Timestamp string `json:"timestamp"` + ChargingStatus float32 `json:"chargingStatus"` + InstantaneousPower int `json:"instantaneousPower"` + RangeHvacOff int `json:"rangeHvacOff"` + BatteryLevel int `json:"batteryLevel"` + BatteryTemperature int `json:"batteryTemperature"` + PlugStatus int `json:"plugStatus"` + LastUpdateTime string `json:"lastUpdateTime"` + ChargePower int `json:"chargePower"` + RemainingTime *int `json:"chargingRemainingTime"` } // Renault is an api.Vehicle implementation for Renault cars @@ -324,6 +324,23 @@ func (v *Renault) ChargeState() (float64, error) { return 0, err } +// Status implements the Vehicle.Status interface +func (v *Renault) Status() (api.ChargeStatus, error) { + status := api.StatusA // disconnected + + res, err := v.apiG() + if res, ok := res.(kamereonResponse); err == nil && ok { + if res.Data.Attributes.PlugStatus > 0 { + status = api.StatusB + } + if res.Data.Attributes.ChargingStatus > 1.0 { + status = api.StatusC + } + } + + return status, 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 990ea323c..34e14cf04 100644 --- a/vehicle/vehicle.go +++ b/vehicle/vehicle.go @@ -24,10 +24,13 @@ 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)" + // Vehicle is an api.Vehicle implementation with configurable getters and setters. type Vehicle struct { *embed chargeG func() (float64, error) + statusG func() (string, error) } func init() { @@ -40,6 +43,7 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error Title string Capacity int64 Charge provider.Config + Status *provider.Config Cache time.Duration }{} @@ -67,10 +71,35 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Vehicle, error chargeG: getter, } - return v, nil + // decorate vehicle with BatterySoC + // decorate vehicle with Status + var status func() (api.ChargeStatus, error) + if cc.Status != nil { + v.statusG, err = provider.NewStringGetterFromConfig(*cc.Status) + if err != nil { + return nil, err + } + status = v.status + } + + res := decorateVehicle(v, status) + + return res, nil } // ChargeState implements the Vehicle.ChargeState interface func (m *Vehicle) ChargeState() (float64, error) { return m.chargeG() } + +// ChargeState implements the Vehicle.ChargeState interface +func (m *Vehicle) status() (api.ChargeStatus, error) { + status := api.StatusF + + statusS, err := m.statusG() + if err == nil { + status = api.ChargeStatus(statusS) + } + + return status, err +} diff --git a/vehicle/vehicle_decorators.go b/vehicle/vehicle_decorators.go new file mode 100644 index 000000000..71de8f871 --- /dev/null +++ b/vehicle/vehicle_decorators.go @@ -0,0 +1,35 @@ +package vehicle + +// Code generated by github.com/andig/cmd/tools/decorate.go. DO NOT EDIT. + +import ( + "github.com/andig/evcc/api" +) + +func decorateVehicle(base api.Vehicle, vehicleStatus func() (api.ChargeStatus, error)) api.Vehicle { + switch { + case vehicleStatus == nil: + return base + + case vehicleStatus != nil: + return &struct { + api.Vehicle + api.VehicleStatus + }{ + Vehicle: base, + VehicleStatus: &decorateVehicleVehicleStatusImpl{ + vehicleStatus: vehicleStatus, + }, + } + } + + return nil +} + +type decorateVehicleVehicleStatusImpl struct { + vehicleStatus func() (api.ChargeStatus, error) +} + +func (impl *decorateVehicleVehicleStatusImpl) Status() (api.ChargeStatus, error) { + return impl.vehicleStatus() +} diff --git a/vehicle/vw/api.go b/vehicle/vw/api.go index 656af4e9d..96f22d459 100644 --- a/vehicle/vw/api.go +++ b/vehicle/vw/api.go @@ -41,6 +41,12 @@ type ChargerResponse struct { StateOfCharge TimedInt RemainingChargingTime TimedInt } + ChargingStatusData struct { + ChargingState TimedString + } + PlugStatusData struct { + PlugState TimedString + } } } } diff --git a/vehicle/vw/impl.go b/vehicle/vw/impl.go index 399e49ce0..5f2c36959 100644 --- a/vehicle/vw/impl.go +++ b/vehicle/vw/impl.go @@ -4,6 +4,7 @@ import ( "strings" "time" + "github.com/andig/evcc/api" "github.com/andig/evcc/provider" ) @@ -51,6 +52,23 @@ func (v *Implementation) FinishTime() (time.Time, error) { return time.Time{}, err } +// Status implements the Vehicle.Status interface +func (v *Implementation) Status() (api.ChargeStatus, error) { + status := api.StatusA // disconnected + + res, err := v.chargerG() + if res, ok := res.(ChargerResponse); err == nil && ok { + if res.Charger.Status.PlugStatusData.PlugState.Content == "connected" { + status = api.StatusB + } + if res.Charger.Status.ChargingStatusData.ChargingState.Content == "charging" { + status = api.StatusC + } + } + + return status, err +} + // Climater implements the Vehicle.Climater interface func (v *Implementation) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) { res, err := v.climateG()