Repository: andig/evcc
Pull request #88 (2020-05-02 09:02:54)

Simplify dummy meter to use power instead of current
Refs #78 and reverted a21b332a20a5e88d58c19360e79b22b5a57c75b7


Patch:
diff --git a/README.md b/README.md
index 2de8b6bb..d6cbab87 100644
--- a/README.md
+++ b/README.md
@@ -305,16 +305,8 @@ Sample configuration:
 ```yaml
 type: mqtt
 topic: mbmd/sdm1-1/Power
-timeout: 30s # don't accept values older than timeout
-scale: 0.001 # floating point factor applied to result, e.g. for kW to W conversion
-```
-
-Sample write configuration:
-
-```yaml
-type: mqtt
-topic: mbmd/charger/maxcurrent
-payload: ${var:%d}
+timeout: 30s
+payload: ${var:%.2f}
 ```
 
 For write access, the data is provided using the `payload` attribute. If `payload` is missing, the value will be written in default format.
@@ -352,7 +344,6 @@ method: GET # default HTTP method
 headers:
 - content-type: application/json
 jq: .data.tuples[0][1] # parse response json
-scale: 0.001 # floating point factor applied to result, e.g. for kW to W conversion
 ```
 
 Sample write configuration:
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 2dde7a6d..ea77d636 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -202,31 +202,30 @@ func (lp *LoadPoint) evChargeStopHandler() {
 	})
 }
 
-// evChargeCurrentHandler updates proxy charge meter's charge current.
-// If physical charge meter is present this handler is not used.
-func (lp *LoadPoint) evChargeCurrentHandler(m *wrapper.ChargeMeter) func(para ...interface{}) {
-	return func(para ...interface{}) {
-		current := para[0].(int64)
-		if !lp.enabled || lp.status != api.StatusC {
-			current = 0
-		}
-		if current > 0 {
-			// limit available power to generation plus consumption/ minus delivery
-			availablePower := math.Abs(lp.pvPower) + lp.availableBatteryPower() + lp.gridPower
-			availableCurrent := int64(powerToCurrent(availablePower, lp.Voltage, lp.Phases))
-			current = min(current, availableCurrent)
-		}
-		m.SetChargeCurrent(current)
-	}
+// consumedPower estimates how much power the charger might have consumed given it was the only load
+// negative values mean pv: production, battery: charging, grid: export
+func consumedPower(pv, battery, grid float64) float64 {
+	return math.Abs(pv) + battery + grid
 }
 
-// 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)
+// evChargeCurrentHandler updates the dummy charge meter's charge power. This simplifies the main flow
+// where the charge meter can always be treated as present.  It assumes that the charge meter cannot consume
+// more than total household consumption. If physical charge meter is present this handler is not used.
+func (lp *LoadPoint) evChargeCurrentHandler(current int64) {
+	power := float64(current*lp.Phases) * lp.Voltage
+
+	if !lp.enabled || lp.status != api.StatusC {
+		// if disabled we cannot be charging
+		power = 0
+	} else if power > 0 && lp.pvMeter != nil {
+		// limit charge power to generation plus grid consumption/ minus grid delivery
+		// as the charger cannot have consumed more than that
+		consumedPower := consumedPower(lp.pvPower, lp.batteryPower, lp.gridPower)
+		power = math.Min(power, consumedPower)
 	}
 
-	return 0
+	// handler only called if charge meter was replaced by dummy
+	lp.chargeMeter.(*wrapper.ChargeMeter).SetPower(power)
 }
 
 // Prepare loadpoint configuration by adding missing helper elements
@@ -243,13 +242,10 @@ func (lp *LoadPoint) Prepare(uiChan chan<- Param, notificationChan chan<- push.E
 		if mt, ok := lp.charger.(api.Meter); ok {
 			lp.chargeMeter = mt
 		} else {
-			mt := &wrapper.ChargeMeter{
-				Phases:  lp.Phases,
-				Voltage: lp.Voltage,
-			}
-			_ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentHandler(mt))
+			mt := &wrapper.ChargeMeter{}
+			_ = lp.bus.Subscribe(evChargeCurrent, lp.evChargeCurrentHandler)
 			_ = lp.bus.Subscribe(evStopCharge, func() {
-				mt.SetChargeCurrent(0)
+				mt.SetPower(0)
 			})
 			lp.chargeMeter = mt
 		}
@@ -456,7 +452,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.availableBatteryPower() - lp.ResidualPower
+	targetPower := lp.chargePower - lp.gridPower - lp.batteryPower - 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
diff --git a/core/wrapper/chargemeter.go b/core/wrapper/chargemeter.go
index d905a6d4..e2dc948c 100644
--- a/core/wrapper/chargemeter.go
+++ b/core/wrapper/chargemeter.go
@@ -8,20 +8,14 @@ import (
 // It uses the charger's actual or max current to calculate power consumption.
 type ChargeMeter struct {
 	sync.Mutex
-	Phases  int64
-	Voltage float64
-	power   float64
+	power float64
 }
 
-func currentToPower(current, voltage float64, phases int64) float64 {
-	return float64(phases) * current * voltage
-}
-
-// SetChargeCurrent updates meter's current power based on actual current
-func (m *ChargeMeter) SetChargeCurrent(current int64) {
+// SetPower updates meter's current power
+func (m *ChargeMeter) SetPower(power float64) {
 	m.Lock()
 	defer m.Unlock()
-	m.power = currentToPower(float64(current), m.Voltage, m.Phases)
+	m.power = power
 }
 
 // CurrentPower implements the Meter.CurrentPower interface
diff --git a/core/wrapper/chargerater.go b/core/wrapper/chargerater.go
index 57beadd8..6084eef4 100644
--- a/core/wrapper/chargerater.go
+++ b/core/wrapper/chargerater.go
@@ -85,7 +85,6 @@ func (cr *ChargeRater) SetChargePower(power float64) {
 
 	// update energy amount if not provided by meter
 	if _, ok := cr.meter.(api.MeterEnergy); !ok {
-		log.DEBUG.Printf("%s set charge power: %.0fW", cr.name, power)
 		// convert power to energy in kWh
 		cr.chargedEnergy += power / 1e3 * float64(cr.clck.Since(cr.start)) / float64(time.Hour)
 		// move timestamp
@@ -109,7 +108,6 @@ func (cr *ChargeRater) ChargedEnergy() (float64, error) {
 		f, err := m.TotalEnergy()
 
 		if err == nil {
-			log.DEBUG.Printf("%s charge energy: %.0fkWh", cr.name, cr.chargedEnergy)
 			return f - cr.startEnergy, nil
 		}
 
diff --git a/provider/config.go b/provider/config.go
index 33ced911..bd21835d 100644
--- a/provider/config.go
+++ b/provider/config.go
@@ -20,7 +20,7 @@ type Config struct {
 // mqttConfig is the specific mqtt getter/setter configuration
 type mqttConfig struct {
 	Topic, Payload string // Payload only applies to setters
-	Scale          float64
+	Multiplier     float64
 	Timeout        time.Duration
 }
 
@@ -42,8 +42,8 @@ func mqttFromConfig(log *util.Logger, other map[string]interface{}) mqttConfig {
 	var pc mqttConfig
 	util.DecodeOther(log, other, &pc)
 
-	if pc.Scale == 0 {
-		pc.Scale = 1
+	if pc.Multiplier == 0 {
+		pc.Multiplier = 1
 	}
 
 	return pc
@@ -67,7 +67,7 @@ func NewFloatGetterFromConfig(log *util.Logger, config Config) (res FloatGetter)
 		res = NewHTTPProviderFromConfig(log, config.Other).FloatGetter
 	case "mqtt":
 		pc := mqttFromConfig(log, config.Other)
-		res = MQTT.FloatGetter(pc.Topic, pc.Scale, pc.Timeout)
+		res = MQTT.FloatGetter(pc.Topic, pc.Multiplier, pc.Timeout)
 	case "script":
 		pc := scriptFromConfig(log, config.Other)
 		res = NewScriptProvider(pc.Timeout).FloatGetter(pc.Cmd)
@@ -90,7 +90,7 @@ func NewIntGetterFromConfig(log *util.Logger, config Config) (res IntGetter) {
 		res = NewHTTPProviderFromConfig(log, config.Other).IntGetter
 	case "mqtt":
 		pc := mqttFromConfig(log, config.Other)
-		res = MQTT.IntGetter(pc.Topic, int64(pc.Scale), pc.Timeout)
+		res = MQTT.IntGetter(pc.Topic, int64(pc.Multiplier), pc.Timeout)
 	case "script":
 		pc := scriptFromConfig(log, config.Other)
 		res = NewScriptProvider(pc.Timeout).IntGetter(pc.Cmd)
diff --git a/provider/http.go b/provider/http.go
index a5806e08..48b884fa 100644
--- a/provider/http.go
+++ b/provider/http.go
@@ -86,7 +86,7 @@ func (p *HTTP) FloatGetter() (float64, error) {
 	}
 
 	f, err := strconv.ParseFloat(s, 64)
-	if err == nil && p.scale != 0 {
+	if err == nil && p.scale > 0 {
 		f *= p.scale
 	}
 
diff --git a/provider/mqtt.go b/provider/mqtt.go
index 6f005d42..966dbfef 100644
--- a/provider/mqtt.go
+++ b/provider/mqtt.go
@@ -105,12 +105,12 @@ func (m *MqttClient) listen(topic string, callback func(string)) {
 }
 
 // FloatGetter creates handler for float64 from MQTT topic that returns cached value
-func (m *MqttClient) FloatGetter(topic string, scale float64, timeout time.Duration) FloatGetter {
+func (m *MqttClient) FloatGetter(topic string, multiplier float64, timeout time.Duration) FloatGetter {
 	h := &msgHandler{
-		log:     m.log,
-		topic:   topic,
-		scale:   scale,
-		timeout: timeout,
+		log:        m.log,
+		topic:      topic,
+		multiplier: multiplier,
+		timeout:    timeout,
 	}
 
 	m.Listen(topic, h.Receive)
@@ -118,12 +118,12 @@ func (m *MqttClient) FloatGetter(topic string, scale float64, timeout time.Durat
 }
 
 // IntGetter creates handler for int64 from MQTT topic that returns cached value
-func (m *MqttClient) IntGetter(topic string, scale int64, timeout time.Duration) IntGetter {
+func (m *MqttClient) IntGetter(topic string, multiplier int64, timeout time.Duration) IntGetter {
 	h := &msgHandler{
-		log:     m.log,
-		topic:   topic,
-		scale:   float64(scale),
-		timeout: timeout,
+		log:        m.log,
+		topic:      topic,
+		multiplier: float64(multiplier),
+		timeout:    timeout,
 	}
 
 	m.Listen(topic, h.Receive)
@@ -213,14 +213,14 @@ func (m *MqttClient) WaitForToken(token mqtt.Token) {
 }
 
 type msgHandler struct {
-	log     *util.Logger
-	once    sync.Once
-	mux     sync.Mutex
-	updated time.Time
-	timeout time.Duration
-	scale   float64
-	topic   string
-	payload string
+	log        *util.Logger
+	once       sync.Once
+	mux        sync.Mutex
+	updated    time.Time
+	timeout    time.Duration
+	multiplier float64
+	topic      string
+	payload    string
 }
 
 func (h *msgHandler) Receive(payload string) {
@@ -263,7 +263,7 @@ func (h *msgHandler) floatGetter() (float64, error) {
 		return 0, fmt.Errorf("%s invalid: '%s'", h.topic, h.payload)
 	}
 
-	return h.scale * val, nil
+	return h.multiplier * val, nil
 }
 
 func (h *msgHandler) intGetter() (int64, error) {


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 39059b70..a0ad214e 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -313,3 +313,30 @@ func TestImmediateOnOff(t *testing.T) {
 		ctrl.Finish()
 	}
 }
+
+func TestConsumedPower(t *testing.T) {
+	tc := []struct {
+		grid, pv, battery, consumed float64
+	}{
+		{0, 0, 0, 0},    // silent night
+		{1, 0, 0, 1},    // grid import
+		{0, 1, 0, 1},    // pv sign ignored
+		{0, -1, 0, 1},   // pv sign ignored
+		{1, 1, 0, 2},    // grid import + pv, pv sign ignored
+		{1, -1, 0, 2},   // grid import + pv, pv sign ignored
+		{0, 0, 1, 1},    // battery discharging
+		{0, 0, -1, -1},  // battery charging -> negative result cannot occur in reality
+		{1, -3, 1, 5},   // grid import + pv + battery discharging
+		{1, -3, -1, 3},  // grid import + pv + battery charging -> should not happen in reality
+		{0, -3, -1, 2},  // pv + battery charging
+		{-1, -4, -1, 2}, // grid export + pv + battery charging
+		{-1, -4, 0, 3},  // grid export + pv
+	}
+
+	for _, tc := range tc {
+		res := consumedPower(tc.pv, tc.battery, tc.grid)
+		if res != tc.consumed {
+			t.Errorf("consumedPower wanted %.f, got %.f", tc.consumed, res)
+		}
+	}
+}
diff --git a/core/wrapper/chargemeter_test.go b/core/wrapper/chargemeter_test.go
index 62468516..a02bd233 100644
--- a/core/wrapper/chargemeter_test.go
+++ b/core/wrapper/chargemeter_test.go
@@ -6,31 +6,18 @@ import (
 	"github.com/golang/mock/gomock"
 )
 
-func TestProxyMeterSinglePhase(t *testing.T) {
+func TestProxyChargeMeter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	tc := []struct {
-		phases  int64
-		voltage float64
-		current int64
-		power   float64
-	}{
-		{1, 100, 10, 1000},
-		{3, 200, 1, 600},
-	}
-
-	for _, tc := range tc {
-		m := ChargeMeter{
-			Phases:  tc.phases,
-			Voltage: tc.voltage,
-		}
+	tc := []float64{600, 1000, 2000}
+	m := ChargeMeter{}
 
-		m.SetChargeCurrent(tc.current)
+	for _, f := range tc {
+		m.SetPower(f)
 
-		if p, err := m.CurrentPower(); p != tc.power || err != nil {
+		if p, err := m.CurrentPower(); p != f || err != nil {
 			t.Errorf("power: %.1f %v", p, err)
 		}
 	}
-
 }