Add battery meter support (#83)

* Add battery meter support

* Clarify configuration
This commit is contained in:
andig 2020-04-30 14:56:26 +02:00 • committed by GitHub
parent 7c62c219af
commit 532e6bdfe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 75 additions and 35 deletions

View file

@ -25,7 +25,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- [Charge Modes](#charge-modes)
- [PV generator configuration](#pv-generator-configuration)
- [Charger configuration](#charger-configuration)
- [Implementation](#implementation)
- [Configuration](#configuration)
- [Charger](#charger)
- [Wallbe hardware preparation](#wallbe-hardware-preparation)
- [OpenWB slave mode](#openwb-slave-mode)
@ -99,6 +99,18 @@ For both PV modes, EVCC needs to assess how much residual PV power is available
In this setup, *residual power* is used as margin to account for fluctuations in PV production that may be faster than EVCC's control loop.
- **Battery meter**: *battery meter* is used if a home battery is installed and you want charging the EV take priority over charging the home battery. As the home battery would otherwise "grab" all available PV power, this meter measures the home battery charging power.
With *grid meter* the charger is then allowed to consume:
Charge Power = Current Charge Power - Grid Meter Power + Battery Meter Power - Residual Power
or without *grid meter*
Charge Power = PV Meter Power + Battery Meter Power - Residual Power
The *battery meter* is expected to deliver negative values when charging, positives values signal discharging and are ignored.
### Charger configuration
When using a *grid meter* for accurate control of PV utilization, EVCC needs to be able to determine the current charge power. There are two configurations for determining the *current charge power*:
@ -109,9 +121,9 @@ If *total energy* is supplied, it can be used to calculate the *charged energy*
- **No charge meter**: If no charge meter is installed, *charge power* is deducted from *charge current* as controlled by the charger. This method is less accurate than using a *charge meter* since the EV may chose to use less power than EVCC has allowed for consumption.
If the charger supplies *total energy* for the charging cycle this value is preferred over the *charge meter*'s value (if present).
## Implementation
## Configuration
EVCC consists of four basic elements: *Charger*, *Meter*, *SoC* and *Loadpoint*. Their APIs are described in [api/api.go](https://github.com/andig/evcc/blob/master/api/api.go).
The EVCC consists of four basic elements: *Charger*, *Meter* and *Vehicle* individually configured and attached to *Loadpoints*.
### Charger

View file

@ -63,20 +63,18 @@ func (lp *LoadPoint) hasChargeMeter() bool {
// Dump loadpoint configuration
func (lp *LoadPoint) Dump() {
vehicle := lp.vehicle != nil
grid := lp.gridMeter != nil
pv := lp.pvMeter != nil
log.INFO.Printf("%s config: vehicle %s grid %s pv %s charge %s", lp.Name,
presence[vehicle],
presence[grid],
presence[pv],
log.INFO.Printf("%s loadpoint config: vehicle %s grid %s pv %s battery %s charge %s", lp.Name,
presence[lp.vehicle != nil],
presence[lp.gridMeter != nil],
presence[lp.pvMeter != nil],
presence[lp.batteryMeter != nil],
presence[lp.hasChargeMeter()],
)
_, power := lp.charger.(api.Meter)
_, energy := lp.charger.(api.ChargeRater)
_, timer := lp.charger.(api.ChargeTimer)
log.INFO.Printf("%s charger: power %s energy %s timer %s", lp.Name,
log.INFO.Printf("%s charger config: power %s energy %s timer %s", lp.Name,
presence[power],
presence[energy],
presence[timer],

View file

@ -47,15 +47,21 @@ type Config struct {
Voltage float64 // Operating voltage. 230V for Germany.
ResidualPower float64 // PV meter only: household usage. Grid meter: household safety margin
ChargerRef string `mapstructure:"charger"` // Charger reference
GridMeterRef string `mapstructure:"gridmeter"` // Grid usage meter reference
PVMeterRef string `mapstructure:"pvmeter"` // PV generation meter reference
ChargeMeterRef string `mapstructure:"chargemeter"` // Charger usage meter reference
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
ChargerRef string `mapstructure:"charger"` // Charger reference
VehicleRef string `mapstructure:"vehicle"` // Vehicle reference
Meters MetersConfig // Meter references
GuardDuration time.Duration // charger enable/disable minimum holding time
}
// MetersConfig contains the loadpoint's meter configuration
type MetersConfig struct {
GridMeterRef string `mapstructure:"grid"` // Grid usage meter reference
ChargeMeterRef string `mapstructure:"charge"` // Charger usage meter reference
PVMeterRef string `mapstructure:"pv"` // PV generation meter reference
BatteryMeterRef string `mapstructure:"battery"` // Battery charging meter reference
}
// LoadPoint is responsible for controlling charge depending on
// SoC needs and power availability.
type LoadPoint struct {
@ -72,11 +78,12 @@ type LoadPoint struct {
chargeRater api.ChargeRater
// meters
charger api.Charger // Charger
gridMeter api.Meter // Grid usage meter
pvMeter api.Meter // PV generation meter
chargeMeter api.Meter // Charger usage meter
vehicle api.Vehicle // Vehicle
charger api.Charger // Charger
gridMeter api.Meter // Grid usage meter
pvMeter api.Meter // PV generation meter
batteryMeter api.Meter // Battery charging meter
chargeMeter api.Meter // Charger usage meter
vehicle api.Vehicle // Vehicle
// cached state
status api.ChargeStatus // Charger status
@ -85,6 +92,7 @@ type LoadPoint struct {
charging bool // Charging cycle
gridPower float64 // Grid power
pvPower float64 // PV power
batteryPower float64 // Battery charge power
chargePower float64 // Charging power
// contactor switch guard
@ -108,17 +116,20 @@ func NewLoadPointFromConfig(log *util.Logger, cp configProvider, other map[strin
} else {
log.FATAL.Fatal("config: missing charger")
}
if lp.PVMeterRef == "" && lp.GridMeterRef == "" {
if lp.Meters.PVMeterRef == "" && lp.Meters.GridMeterRef == "" {
log.FATAL.Fatal("config: missing either pv or grid meter")
}
if lp.GridMeterRef != "" {
lp.gridMeter = cp.Meter(lp.GridMeterRef)
if lp.Meters.GridMeterRef != "" {
lp.gridMeter = cp.Meter(lp.Meters.GridMeterRef)
}
if lp.ChargeMeterRef != "" {
lp.chargeMeter = cp.Meter(lp.ChargeMeterRef)
if lp.Meters.ChargeMeterRef != "" {
lp.chargeMeter = cp.Meter(lp.Meters.ChargeMeterRef)
}
if lp.PVMeterRef != "" {
lp.pvMeter = cp.Meter(lp.PVMeterRef)
if lp.Meters.PVMeterRef != "" {
lp.pvMeter = cp.Meter(lp.Meters.PVMeterRef)
}
if lp.Meters.BatteryMeterRef != "" {
lp.batteryMeter = cp.Meter(lp.Meters.BatteryMeterRef)
}
if lp.VehicleRef != "" {
lp.vehicle = cp.Vehicle(lp.VehicleRef)
@ -201,7 +212,7 @@ func (lp *LoadPoint) evChargeCurrentHandler(m *wrapper.ChargeMeter) func(para ..
}
if current > 0 {
// limit available power to generation plus consumption/ minus delivery
availablePower := math.Abs(lp.pvPower) + lp.gridPower
availablePower := math.Abs(lp.pvPower) + lp.availableBatteryPower() + lp.gridPower
availableCurrent := int64(powerToCurrent(availablePower, lp.Voltage, lp.Phases))
current = min(current, availableCurrent)
}
@ -209,6 +220,15 @@ func (lp *LoadPoint) evChargeCurrentHandler(m *wrapper.ChargeMeter) func(para ..
}
}
// availableBatteryPower delivers the battery charging power as additional available power at the grid connection point
func (lp *LoadPoint) availableBatteryPower() float64 {
if lp.batteryPower < 0 {
return math.Abs(lp.batteryPower)
}
return 0
}
// Prepare loadpoint configuration by adding missing helper elements
func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.Event) {
lp.notificationChan = notificationChan
@ -436,7 +456,7 @@ func (lp *LoadPoint) rampOn(target int64) error {
// updateModePV sets "minpv" or "pv" load modes
func (lp *LoadPoint) updateModePV(mode api.ChargeMode) error {
// grid meter will always be available, if as wrapped pv meter
targetPower := lp.chargePower - lp.gridPower - lp.ResidualPower
targetPower := lp.chargePower - lp.gridPower + lp.availableBatteryPower() - lp.ResidualPower
log.DEBUG.Printf("%s target power: %.0fW = %.0fW charge - %.0fW grid - %.0fW residual", lp.Name, targetPower, lp.chargePower, lp.gridPower, lp.ResidualPower)
// get max charge current
@ -496,6 +516,7 @@ func (lp *LoadPoint) updateMeters() (err error) {
// read PV meter before charge meter
retryMeter("grid", lp.gridMeter, &lp.gridPower)
retryMeter("pv", lp.pvMeter, &lp.pvPower)
retryMeter("battery", lp.batteryMeter, &lp.batteryPower)
retryMeter("charge", lp.chargeMeter, &lp.chargePower)
return err

View file

@ -55,6 +55,12 @@ meters:
type: mqtt
topic: mbmd/sdm1-2/Power
timeout: 10s # don't use older values
- name: battery
type: default
power:
type: mqtt
topic: mbmd/sma1-1/Power
timeout: 10s # don't use older values
- name: charge
type: default
power:
@ -79,8 +85,9 @@ chargers:
uri: 192.168.0.8:502 # ModBus address
# legacy: true # enable for older Wallbes with Phoenix EV-CC-AC1-M3-CBC-RCM controller
- name: phoenix
type: phoenix # Charger with Phoenix Contact controller
type: phoenix-emcp # Charger with Phoenix Contact controller
uri: 192.168.0.8:502 # ModBus address
id: 1
- name: simpleevse-tcp
type: simpleevse # Charger with Phoenix Contact controller
uri: 192.168.0.8:502 # TCP ModBus address
@ -169,10 +176,12 @@ loadpoints:
- name: main # name for logging
vehicle: audi
charger: wallbe # charger
gridmeter: grid # grid meter
pvmeter: pv # pv meter
chargemeter: charge # charge meter
sensitivity: 1 # current raise/lower steps size (default 1A)
meters:
grid: grid # grid meter
pv: pv # pv meter
battery: battery # battery meter
charge: charge # charge meter
guardduration: 10m # switch charger contactor not more often than this (default 10m)
maxcurrent: 16 # maximum charge current (default 16A)
phases: 3 # ev phases (default 3)
sensitivity: 1 # current raise/lower step size (default 10A)