Repository: evcc-io/evcc
Pull request #12847 (2024-03-10 13:20:40)

Restore logic for chargers that disable themselves to be able to switch phases
Fix https://github.com/evcc-io/evcc/issues/12825

andig: > Fix for: https://github.com/evcc-io/evcc/issues/12825

@MarkusGH einfach "fix #12825", sonst wird das nicht sauber verlinkt.
MarkusGH: Wobei man syncCharger einfach mal gründlich ausmisten sollte.
Das ist viel Code für eine mittlerweile im Grunde einfache Logik.
Soll ich einen Vorschlag machen, wenn der PR durch ist?
andig: Sehr gerne- ich bekomme da auch immer graue Haare. Hier hätte ich gerne noch eine Meinung von @GrimmiMeloni 
MarkusGH: @GrimmiMeloni: Ist es für die Easee OK, wenn sie unter Umständen gleich nach dem Pausieren wieder Resumed wird? Denn das passiert jetzt im nächsten Regelintervall, gleich nach der Phasenumschaltung. "Früher" war da eine Mindestverzögerung von 30 Sekunden drin, die ich als "Angstlösung" eingebaut hatte.

Patch:
diff --git a/README.md b/README.md
index ce434c30..4af0c6e1 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,7 @@ make ui build
 
 ### Code formatting
 
-We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Veture](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running:
+We use linters (golangci-lint, Prettier) to keep a coherent source code formatting. It's recommended to use the format-on-save feature of your editor. For VSCode use the [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) and [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur) extension. You can manually reformat your code by running:
 
 ```sh
 make lint
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 9404c370..b98c6758 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -174,6 +174,9 @@ func NewOCPP(id string, connector int, idtag string,
 			meterInterval = 10 * time.Second
 		}
 	} else {
+		// fix timing issue in EVBox when switching OCPP protocol version
+		time.Sleep(time.Second)
+
 		err := ocpp.Instance().GetConfiguration(cp.ID(), func(resp *core.GetConfigurationConfirmation, err error) {
 			if err == nil {
 				// log unsupported configuration keys
@@ -335,8 +338,8 @@ func (c *OCPP) Enable(enable bool) (err error) {
 
 	if enable {
 		if txn > 0 {
-			// we have the transaction id, switch from 0A to last current
-			return c.updatePeriod(c.current)
+			// we have the transaction id, treat as enabled
+			return nil
 		}
 
 		err = ocpp.Instance().RemoteStartTransaction(c.conn.ChargePoint().ID(), func(resp *core.RemoteStartTransactionConfirmation, err error) {
@@ -350,25 +353,31 @@ func (c *OCPP) Enable(enable bool) (err error) {
 			request.ConnectorId = &connector
 			request.ChargingProfile = c.getTxChargingProfile(c.current, 0)
 		})
+	} else {
+		// if no transaction is running, the vehicle may have stopped it (which is ok) or an unknown transaction is running
+		if txn == 0 {
+			// we cannot tell if a transaction is really running, so we check the status
+			status, err := c.Status()
+			if err != nil {
+				return err
+			}
+			if status == api.StatusC {
+				return errors.New("cannot disable: unknown transaction running")
+			}
 
-		return c.wait(err, rc)
-	}
-
-	// if no transaction is running, the vehicle may have stopped it (which is ok) or an unknown transaction is running
-	if txn == 0 {
-		// we cannot tell if a transaction is really running, so we check the status
-		status, err := c.Status()
-		if err != nil {
-			return err
-		}
-		if status == api.StatusC {
-			return errors.New("cannot disable: unknown transaction running")
+			return nil
 		}
 
-		return nil
+		err = ocpp.Instance().RemoteStopTransaction(c.conn.ChargePoint().ID(), func(resp *core.RemoteStopTransactionConfirmation, err error) {
+			if err == nil && resp != nil && resp.Status != types.RemoteStartStopStatusAccepted {
+				err = errors.New(string(resp.Status))
+			}
+
+			rc <- err
+		}, txn)
 	}
 
-	return c.updatePeriod(0)
+	return c.wait(err, rc)
 }
 
 func (c *OCPP) setChargingProfile(profile *types.ChargingProfile) error {
diff --git a/charger/twc3.go b/charger/twc3.go
index e383950a..c590637d 100644
--- a/charger/twc3.go
+++ b/charger/twc3.go
@@ -16,10 +16,7 @@ import (
 
 // Twc3 is an api.Vehicle implementation for Twc3 cars
 type Twc3 struct {
-	*request.Helper
-	log     *util.Logger
 	lp      loadpoint.API
-	uri     string
 	vitalsG func() (Vitals, error)
 	enabled bool
 }
@@ -71,20 +68,16 @@ func NewTwc3FromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	log := util.NewLogger("twc3")
+	c := &Twc3{}
 
-	c := &Twc3{
-		Helper: request.NewHelper(log),
-		uri:    util.DefaultScheme(strings.TrimSuffix(cc.URI, "/"), "http"),
-		log:    log,
-	}
+	client := request.NewHelper(util.NewLogger("twc3"))
+	uri := fmt.Sprintf("%s/api/1/vitals", util.DefaultScheme(strings.TrimSuffix(cc.URI, "/"), "http"))
 
 	c.vitalsG = provider.Cached(func() (Vitals, error) {
 		var res Vitals
-		uri := fmt.Sprintf("%s/api/1/vitals", c.uri)
-		err := c.GetJSON(uri, &res)
+		err := client.GetJSON(uri, &res)
 		return res, err
-	}, time.Second)
+	}, cc.Cache)
 
 	return c, nil
 }
diff --git a/cmd/check_config.go b/cmd/check_config.go
index ad0cc23e..da265bb2 100644
--- a/cmd/check_config.go
+++ b/cmd/check_config.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	_ "embed"
+	"fmt"
 	"os"
 
 	"github.com/spf13/cobra"
@@ -24,6 +25,6 @@ func runConfigCheck(cmd *cobra.Command, args []string) {
 		log.FATAL.Println("config invalid:", err)
 		os.Exit(1)
 	} else {
-		log.INFO.Println("config valid")
+		fmt.Println("config valid")
 	}
 }
diff --git a/core/loadpoint.go b/core/loadpoint.go
index cb8df4d1..7d769c3e 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -658,6 +658,11 @@ func (lp *Loadpoint) syncCharger() error {
 		return err
 	}
 
+	// some chargers (i.E. Easee in some configurations) disable themself to be able to switch phases
+	if !enabled && lp.enabled && !lp.phaseSwitchCompleted() {
+		return lp.charger.Enable(true) // enable charger
+	}
+
 	if lp.chargerUpdateCompleted() {
 		defer func() {
 			lp.enabled = enabled
diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go
index 57a441f6..73ee4468 100644
--- a/core/loadpoint_effective.go
+++ b/core/loadpoint_effective.go
@@ -60,26 +60,29 @@ func (lp *Loadpoint) SocBasedPlanning() bool {
 
 // effectiveMinCurrent returns the effective min current
 func (lp *Loadpoint) effectiveMinCurrent() float64 {
-	minCurrent := lp.GetMinCurrent()
+	lpMin := lp.GetMinCurrent()
+	var vehicleMin, chargerMin float64
 
 	if v := lp.GetVehicle(); v != nil {
 		if res, ok := v.OnIdentified().GetMinCurrent(); ok {
-			minCurrent = max(minCurrent, res)
+			vehicleMin = res
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
 		if res, _, err := c.GetMinMaxCurrent(); err == nil {
-			if res > 0 && res < minCurrent {
-				minCurrent = res
-			} else {
-				minCurrent = max(minCurrent, res)
-			}
-			lp.publish(keys.EffectiveMinCurrent, minCurrent)
+			chargerMin = res
 		}
 	}
 
-	return minCurrent
+	switch {
+	case max(vehicleMin, chargerMin) == 0:
+		return lpMin
+	case chargerMin > 0:
+		return max(vehicleMin, chargerMin)
+	default:
+		return max(vehicleMin, lpMin)
+	}
 }
 
 // effectiveMaxCurrent returns the effective max current
@@ -87,15 +90,14 @@ func (lp *Loadpoint) effectiveMaxCurrent() float64 {
 	maxCurrent := lp.GetMaxCurrent()
 
 	if v := lp.GetVehicle(); v != nil {
-		if res, ok := v.OnIdentified().GetMaxCurrent(); ok {
+		if res, ok := v.OnIdentified().GetMaxCurrent(); ok && res > 0 {
 			maxCurrent = min(maxCurrent, res)
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
-		if _, res, err := c.GetMinMaxCurrent(); err == nil {
+		if _, res, err := c.GetMinMaxCurrent(); err == nil && res > 0 {
 			maxCurrent = min(maxCurrent, res)
-			lp.publish(keys.EffectiveMaxCurrent, maxCurrent)
 		}
 	}
 
diff --git a/packaging/scripts/postremove.sh b/packaging/scripts/postremove.sh
index 6ef3b8ce..57e207b9 100644
--- a/packaging/scripts/postremove.sh
+++ b/packaging/scripts/postremove.sh
@@ -18,51 +18,38 @@ if [ "$1" = "purge" ]; then
 	fi
 fi
 
-
-
-
-# if interactive: Call /usr/bin/evcc checkconfig and check the return code (newer version)
+# if interactive: call `/usr/bin/evcc checkconfig` and check the return code (newer version)
 # if return code is 0, do nothing
 # else: Ask user if he wants to keep the old version (working) or the new version (not working) 
 # Remember the choice with /tmp/.evccrollback and fail new-postrm failed-upgrade old-version new-version to initiate dpkg's rollback
-if [ "$1" = "upgrade" ]; then
-	INTERACTIVE=0
-	# is shell script interactive?
-	if [ -t 0 ]; then
-	  INTERACTIVE=1
-	else
-	  INTERACTIVE=0
+if [ "$1" = "upgrade" ] && [ -t 0 ]; then
+	if ! /usr/bin/evcc checkconfig > /dev/null; then
+		echo "--------------------------------------------------------------------------------"
+		echo "ERROR: your configuration is not compatible with the new version:"
+		/usr/bin/evcc checkconfig --log error || true
+		echo "Please consult the release notes: https://github.com/evcc-io/evcc/releases"
+		echo "--------------------------------------------------------------------------------"
+
+		while true; do
+			echo "Do you want to keep your old (working) version? [Y/n]: "
+			read choice
+			case "$choice" in
+				n*|N*|"")
+					echo "We will keep the new version. Your configuration stays untouched!"
+					break
+					;;
+				y*|Y*)
+					echo "The old version will be restored. Your configuration stays untouched! Following errors are intended:"
+					touch /tmp/.evccrollback
+					exit 1
+					break
+					;;
+				*)
+					;;
+			esac
+		done
 	fi
-
-	if [ $INTERACTIVE -eq 1 ]; then
-      if ! /usr/bin/evcc checkconfig > /dev/null; then
-			echo "--------------------------------------------------------------------------------"
-			echo "ERROR: your evcc configuration is not compatible with the new version. Please consider reading the release notes: https://github.com/evcc-io/evcc/releases"
-			echo "checkconfig Output:" 
-			/usr/bin/evcc checkconfig || true
-			echo "--------------------------------------------------------------------------------"
-	
-			while true; do
-				echo "Do you want to keep your old (working) evcc version? [Y/n]: "
-				read choice
-				case "$choice" in
-					n*|N*|"")
-						echo "We will keep the new version. Your evcc configuration stays untouched!"
-						break
-						;;
-					y*|Y*)
-						echo "The old version will be restored. Your evcc configuration stays untouched! Following errors are intended:"
-						touch /tmp/.evccrollback
-						exit 1
-						break
-						;;
-					*)
-						;;
-				esac
-			done
-		fi
-	fi 
-fi
+fi 
 
 # if upgrade goal fails, new-postrm failed-upgrade old-version new-version is called. It should fail to initiate rollback
 if [ "$1" = "failed-upgrade" ]; then
diff --git a/templates/definition/charger/ocpp-elvi.yaml b/templates/definition/charger/ocpp-elvi.yaml
index 6b54a167..dd168c1a 100644
--- a/templates/definition/charger/ocpp-elvi.yaml
+++ b/templates/definition/charger/ocpp-elvi.yaml
@@ -27,4 +27,3 @@ render: |
   metervalues: Current.Import.L1,Current.Import.L2,Current.Import.L3,Energy.Active.Import.Register,Power.Active.Import,Voltage.L1,Voltage.L2,Voltage.L3
   meterinterval: {{ .meterinterval }}
   {{- end }}
-  getconfiguration: false
diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml
index 4189506f..1c69d86d 100644
--- a/templates/definition/meter/goodwe-hybrid.yaml
+++ b/templates/definition/meter/goodwe-hybrid.yaml
@@ -34,37 +34,17 @@ render: |
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
-    source: calc
-    add:
-    - source: modbus
-      {{- include "modbus" . | indent 4 }}
-      register: # manual non-sunspec register configuration
-        address: 35105 # Ppv1 PV1 Power
-        type: holding
-        decode: uint32nan
-    - source: modbus
-      {{- include "modbus" . | indent 4 }}
-      register: # manual non-sunspec register configuration
-        address: 35109 # Ppv2 PV2 Power
-        type: holding
-        decode: uint32nan
-    - source: modbus
-      {{- include "modbus" . | indent 4 }}
-      register: # manual non-sunspec register configuration
-        address: 35113 # Ppv3 PV3 Power
-        type: holding
-        decode: uint32nan
-    - source: modbus
-      {{- include "modbus" . | indent 4 }}
-      register: # manual non-sunspec register configuration
-        address: 35117 # Ppv4 PV4 Power
-        type: holding
-        decode: uint32nan
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register: # manual non-sunspec register configuration
+      address: 35301 # PV Total Power
+      type: holding
+      decode: uint32nan
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 35191 # PV E-Total Total PV Energy
+      address: 35191 # PV Energy-Total
       type: holding
       decode: uint32
     scale: 0.1
@@ -74,21 +54,21 @@ render: |
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 35183 # Pbattery1 First group battery power
+      address: 35182 # Battery1 Power
       type: holding
-      decode: int16
+      decode: int32
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 37007 # SOC First group battery capacity
+      address: 37007 # SOC
       type: holding
       decode: uint16
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register: # manual non-sunspec register configuration
-      address: 35209 # E-BatteryDischarge Discharge energy
+      address: 35209 # Energy-Battery Discharge
       type: holding
       decode: uint32
     scale: 0.1
diff --git a/util/cloud/client.go b/util/cloud/client.go
index 84a5ec2a..414da155 100644
--- a/util/cloud/client.go
+++ b/util/cloud/client.go
@@ -3,6 +3,7 @@ package cloud
 import (
 	"crypto/tls"
 	_ "embed"
+	"net"
 
 	"github.com/evcc-io/evcc/util"
 	"google.golang.org/grpc"
@@ -10,7 +11,7 @@ import (
 )
 
 var (
-	host = util.Getenv("GRPC_URI", "sponsor.evcc.io:8080")
+	hostport = util.Getenv("GRPC_URI", "sponsor.evcc.io:8080")
 
 	conn *grpc.ClientConn
 )
@@ -20,9 +21,15 @@ func Connection() (*grpc.ClientConn, error) {
 		return conn, nil
 	}
 
-	var err error
-	creds := credentials.NewTLS(new(tls.Config))
-	conn, err = grpc.Dial(host, grpc.WithTransportCredentials(creds))
+	host, _, err := net.SplitHostPort(hostport)
+	if err != nil {
+		return nil, err
+	}
+
+	creds := credentials.NewTLS(&tls.Config{
+		ServerName: host,
+	})
+	conn, err = grpc.Dial(hostport, grpc.WithTransportCredentials(creds))
 
 	return conn, err
 }


Test patch:
diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go
index 52e2957a..f2165478 100644
--- a/core/loadpoint_effective_test.go
+++ b/core/loadpoint_effective_test.go
@@ -21,12 +21,16 @@ func TestEffectiveMinMaxCurrent(t *testing.T) {
 		effectiveMin, effectiveMax float64
 	}{
 		{0, 0, 0, 0, 6, 16},
-		{1, 10, 0, 0, 1, 10},     // charger lower
-		{10, 20, 0, 0, 10, 16},   // charger higher - max ignored
-		{0, 0, 1, 10, 6, 10},     // vehicle lower - min ignored
-		{0, 0, 10, 20, 10, 16},   // vehicle higher - max ignored
-		{1, 10, 2, 12, 1, 10},    // charger + vehicle lower
-		{10, 20, 12, 22, 10, 16}, // charger + vehicle higher
+		{2, 0, 0, 0, 2, 16},   // charger min lower, max empty - charger wins
+		{7, 0, 0, 0, 7, 16},   // charger min higher, max empty (no practical use)
+		{0, 10, 0, 0, 6, 10},  // charger max lower, min empty - loadpoint wins
+		{0, 20, 0, 0, 6, 16},  // charger max higher, min empty - loadpoint wins
+		{0, 0, 5, 0, 6, 16},   // vehicle min lower, max empty - loadpoint wins
+		{0, 0, 8, 0, 8, 16},   // vehicle min higher, max empty - vehicle wins
+		{0, 0, 0, 10, 6, 10},  // vehicle max lower, min empty - vehicle wins
+		{0, 0, 0, 20, 6, 16},  // vehicle max higher, min empty - loadpoint wins
+		{2, 0, 5, 0, 5, 16},   // charger + vehicle min lower, max empty - vehicle wins
+		{0, 20, 0, 32, 6, 16}, // charger + vehicle max higher, min empty - loadpoint wins
 	}
 
 	for _, tc := range tc {
@@ -60,7 +64,7 @@ func TestEffectiveMinMaxCurrent(t *testing.T) {
 			lp.vehicle = vehicle
 		}
 
-		assert.Equal(t, tc.effectiveMin, lp.effectiveMinCurrent())
-		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent())
+		assert.Equal(t, tc.effectiveMin, lp.effectiveMinCurrent(), "min")
+		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent(), "max")
 	}
 }