Repository: evcc-io/evcc
Pull request #11347 (2023-12-30 12:45:31)

chore: refactor reading phase values
/cc @thierolm für Tasmota


Patch:
diff --git a/charger/hesotec.go b/charger/hesotec.go
index 1b591087..00a36cef 100644
--- a/charger/hesotec.go
+++ b/charger/hesotec.go
@@ -180,31 +180,26 @@ var _ api.PhaseCurrents = (*Hesotec)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *Hesotec) Currents() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(hesotecRegCurrents, 6)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var res [3]float64
-	for i := range res {
-		res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / 1e3
-	}
-
-	return res[0], res[1], res[2], nil
+	return wb.getPhaseValues(hesotecRegCurrents, 2, 1e3)
 }
 
 var _ api.PhaseVoltages = (*Hesotec)(nil)
 
 // Voltages implements the api.PhaseVoltages interface
 func (wb *Hesotec) Voltages() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(hesotecRegVoltages, 3)
+	return wb.getPhaseValues(hesotecRegVoltages, 1, 1)
+}
+
+// getPhaseValues returns 3 sequential phase values
+func (wb *Hesotec) getPhaseValues(reg, width uint16, divider float64) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(reg, 3*width)
 	if err != nil {
 		return 0, 0, 0, err
 	}
 
 	var res [3]float64
 	for i := range res {
-		res[i] = float64(binary.BigEndian.Uint16(b[2*i:]))
+		res[i] = float64(binary.BigEndian.Uint32(b[2*int(width)*i:])) / divider
 	}
 
 	return res[0], res[1], res[2], nil
diff --git a/charger/huawei-ac.go b/charger/huawei-ac.go
index d41b1d65..def40557 100644
--- a/charger/huawei-ac.go
+++ b/charger/huawei-ac.go
@@ -84,13 +84,9 @@ func NewHuaweiACCharger(uri string, slaveID uint8) (api.Charger, error) {
 }
 
 func (wb *HuaweiACCharger) setCurrent(current float64) error {
-	var phases int
-	// get (expectedly) active phases from loadpoint
+	phases := 3
 	if wb.lp != nil {
-		phases = wb.lp.GetPhases()
-	}
-	if phases == 0 {
-		phases = 3
+		phases = wb.lp.ActivePhases()
 	}
 
 	b := make([]byte, 4)
diff --git a/charger/phoenix-charx.go b/charger/phoenix-charx.go
index 2ae37c03..7b3357cb 100644
--- a/charger/phoenix-charx.go
+++ b/charger/phoenix-charx.go
@@ -212,22 +212,17 @@ func (wb *PhoenixCharx) totalEnergy() (float64, error) {
 
 // currents implements the api.PhaseCurrents interface
 func (wb *PhoenixCharx) currents() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegCurrents), 3*2)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var res [3]float64
-	for i := range res {
-		res[i] = float64(encoding.Int32(b[4*i:])) / 1e3
-	}
-
-	return res[0], res[1], res[2], nil
+	return wb.getPhaseValues(charxRegCurrents)
 }
 
 // voltages implements the api.PhaseVoltages interface
 func (wb *PhoenixCharx) voltages() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegVoltages), 3*2)
+	return wb.getPhaseValues(charxRegVoltages)
+}
+
+// getPhaseValues returns 3 sequential phase values
+func (wb *PhoenixCharx) getPhaseValues(reg uint16) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wb.register(reg), 6)
 	if err != nil {
 		return 0, 0, 0, err
 	}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index fbe24e6d..7b302e5b 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -534,7 +534,7 @@ func (lp *Loadpoint) evChargeCurrentHandler(current float64) {
 // If physical charge meter is present this handler is not used.
 // The actual value is published by the evChargeCurrentHandler
 func (lp *Loadpoint) evChargeCurrentWrappedMeterHandler(current float64) {
-	power := current * float64(lp.activePhases()) * Voltage
+	power := current * float64(lp.ActivePhases()) * Voltage
 
 	// if disabled we cannot be charging
 	if !lp.enabled || !lp.charging() {
@@ -582,7 +582,7 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 
 	lp.setConfiguredPhases(lp.ConfiguredPhases)
 	lp.publish(keys.PhasesEnabled, lp.phases)
-	lp.publish(keys.PhasesActive, lp.activePhases())
+	lp.publish(keys.PhasesActive, lp.ActivePhases())
 	lp.publishTimer(phaseTimer, 0, timerInactive)
 	lp.publishTimer(pvTimer, 0, timerInactive)
 	lp.publishTimer(guardTimer, 0, timerInactive)
@@ -1052,7 +1052,7 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
 	}
 
 	var waiting bool
-	activePhases := lp.activePhases()
+	activePhases := lp.ActivePhases()
 	availablePower := lp.chargePower - sitePower
 	scalable := (sitePower > 0 || !lp.enabled) && activePhases > 1 && lp.ConfiguredPhases < 3
 
@@ -1156,7 +1156,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
 
 	// calculate target charge current from delta power and actual current
 	effectiveCurrent := lp.effectiveCurrent()
-	activePhases := lp.activePhases()
+	activePhases := lp.ActivePhases()
 	deltaCurrent := powerToCurrent(-sitePower, activePhases)
 	targetCurrent := max(effectiveCurrent+deltaCurrent, 0)
 
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index d3d480fc..9f245a06 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -49,6 +49,8 @@ type API interface {
 	GetPhases() int
 	// SetPhases sets the enabled phases
 	SetPhases(int) error
+	// ActivePhases returns the active phases for the current vehicle
+	ActivePhases() int
 
 	// GetLimitSoc returns the session limit soc
 	GetLimitSoc() int
diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go
index 091a6593..8138ff1a 100644
--- a/core/loadpoint/mock.go
+++ b/core/loadpoint/mock.go
@@ -35,6 +35,20 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
 	return m.recorder
 }
 
+// ActivePhases mocks base method.
+func (m *MockAPI) ActivePhases() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ActivePhases")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// ActivePhases indicates an expected call of ActivePhases.
+func (mr *MockAPIMockRecorder) ActivePhases() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivePhases", reflect.TypeOf((*MockAPI)(nil).ActivePhases))
+}
+
 // EffectiveMaxPower mocks base method.
 func (m *MockAPI) EffectiveMaxPower() float64 {
 	m.ctrl.T.Helper()
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index 61cc6d65..e82b8801 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -44,7 +44,7 @@ func (lp *Loadpoint) resetMeasuredPhases() {
 	lp.measuredPhases = 0
 	lp.Unlock()
 
-	lp.publish(keys.PhasesActive, lp.activePhases())
+	lp.publish(keys.PhasesActive, lp.ActivePhases())
 }
 
 // getMeasuredPhases provides synchronized access to measuredPhases
@@ -64,9 +64,9 @@ func expect(phases int) int {
 	return unknownPhases
 }
 
-// activePhases returns the number of expectedly active phases for the meter.
+// ActivePhases returns the number of expectedly active phases for the meter.
 // If unknown for 1p3p chargers during startup it will assume 3p.
-func (lp *Loadpoint) activePhases() int {
+func (lp *Loadpoint) ActivePhases() int {
 	physical := lp.GetPhases()
 	vehicle := lp.getVehiclePhases()
 	measured := lp.getMeasuredPhases()
diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go
index 94af1d3c..673c18f9 100644
--- a/core/loadpoint_vehicle.go
+++ b/core/loadpoint_vehicle.go
@@ -156,7 +156,7 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) {
 	}
 
 	// re-publish vehicle settings
-	lp.publish(keys.PhasesActive, lp.activePhases())
+	lp.publish(keys.PhasesActive, lp.ActivePhases())
 	lp.unpublishVehicle()
 
 	// publish effective values
diff --git a/meter/tasmota/connection.go b/meter/tasmota/connection.go
index 438e5626..818831bf 100644
--- a/meter/tasmota/connection.go
+++ b/meter/tasmota/connection.go
@@ -220,38 +220,36 @@ func (c *Connection) TotalEnergy() (float64, error) {
 
 // Currents implements the api.PhaseCurrents interface
 func (c *Connection) Currents() (float64, float64, float64, error) {
-	s, err := c.statusSnsG.Get()
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var res [3]float64
-	for i, c := range c.channels {
-		res[i], err = s.StatusSNS.Energy.Current.Channel(c)
-		if err != nil {
-			return 0, 0, 0, err
-		}
-	}
-
-	return res[0], res[1], res[2], err
+	return c.getPhaseValues(func(s StatusSNSResponse) Channels {
+		return s.StatusSNS.Energy.Current
+	})
 }
 
 // Voltages implements the api.PhaseVoltages interface
 func (c *Connection) Voltages() (float64, float64, float64, error) {
+	return c.getPhaseValues(func(s StatusSNSResponse) Channels {
+		return s.StatusSNS.Energy.Voltage
+	})
+}
+
+// getPhaseValues returns 3 sequential phase values
+func (c *Connection) getPhaseValues(fun func(StatusSNSResponse) Channels) (float64, float64, float64, error) {
 	s, err := c.statusSnsG.Get()
 	if err != nil {
 		return 0, 0, 0, err
 	}
 
+	all := fun(s)
+
 	var res [3]float64
-	for i, c := range c.channels {
-		res[i], err = s.StatusSNS.Energy.Voltage.Channel(c)
+	for i := range res {
+		res[i], err = all.Channel(c.channels[i])
 		if err != nil {
 			return 0, 0, 0, err
 		}
 	}
 
-	return res[0], res[1], res[2], err
+	return res[0], res[1], res[2], nil
 }
 
 // SmlPower provides the sml sensor power
diff --git a/meter/tasmota/types.go b/meter/tasmota/types.go
index 4435ee82..e8cba524 100644
--- a/meter/tasmota/types.go
+++ b/meter/tasmota/types.go
@@ -92,7 +92,7 @@ type StatusSNSResponse struct {
 	}
 }
 
-// Channels is a Tasmota specifc helper type to handle meter value lists and single meter values
+// Channels is a Tasmota specific helper type to handle meter value lists and single meter values
 type Channels []float64
 
 func (ch *Channels) Channel(channel int) (float64, error) {


Test patch:
diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go
index 52fa6cdd..2c92c068 100644
--- a/core/loadpoint_phases_test.go
+++ b/core/loadpoint_phases_test.go
@@ -125,7 +125,7 @@ func TestMaxActivePhases(t *testing.T) {
 func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string, tc testCase) {
 	t.Helper()
 
-	act := lp.activePhases()
+	act := lp.ActivePhases()
 	max := lp.maxActivePhases()
 
 	testDirection := direction[0:1] // (d)own or (u)p
@@ -221,7 +221,7 @@ func TestPvScalePhases(t *testing.T) {
 			t.Error("wrong phases", lp.phases, tc.physical)
 		}
 
-		if phs := lp.activePhases(); phs != tc.actExpected {
+		if phs := lp.ActivePhases(); phs != tc.actExpected {
 			t.Errorf("expected active %d, got %d", tc.actExpected, phs)
 		}
 		if phs := lp.maxActivePhases(); phs != tc.maxExpected {