Repository: evcc-io/evcc
Pull request #6434 (2023-02-25 12:11:48)

Improve websocket handling


Patch:
diff --git a/assets/js/components/VehicleStatus.vue b/assets/js/components/VehicleStatus.vue
index 029c454b..ed76cdcf 100644
--- a/assets/js/components/VehicleStatus.vue
+++ b/assets/js/components/VehicleStatus.vue
@@ -26,6 +26,7 @@ export default {
 	computed: {
 		phaseTimerActive() {
 			return (
+				this.charging &&
 				this.phaseRemainingInterpolated > 0 &&
 				["scale1p", "scale3p"].includes(this.phaseAction)
 			);
diff --git a/i18n/en.toml b/i18n/en.toml
index 2d333d5c..159053eb 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -182,8 +182,8 @@ disconnected = "Disconnected."
 minCharge = "Minimum charging to {soc}%."
 pvDisable = "Not enough surplus. Pausing in {remaining}…"
 pvEnable = "Surplus available. Starting in {remaining}…"
-scale1p = "Reducing to 1-phase charging in {remaining}…"
-scale3p = "Increasing to 3-phase charging in {remaining}…"
+scale1p = "Reducing to 1-phase power in {remaining}…"
+scale3p = "Increasing to 3-phase power in {remaining}…"
 targetChargeActive = "Target charge active…"
 targetChargePlanned = "Target charging starts at {time}."
 targetChargeWaitForVehicle = "Target charge ready. Waiting for vehicle…"
diff --git a/packaging/scripts/postinstall.sh b/packaging/scripts/postinstall.sh
index e484d920..67c3d10b 100644
--- a/packaging/scripts/postinstall.sh
+++ b/packaging/scripts/postinstall.sh
@@ -4,7 +4,7 @@ set -e
 USER_CHOICE_CONFIG="/etc/evcc-userchoices.sh"
 ETC_SERVICE="/etc/systemd/system/evcc.service"
 USR_LOCAL_BIN="/usr/local/bin/evcc"
-RESTART_FLAG_FILE="/tmp/.restartEvccOnUpgrade"
+RESTART_FLAG_FILE=/var/lib/evcc/.restartOnUpgrade
 
 # Usage: askUserKeepFile <file>
 # Return: 1 = keep, 0 = delete
@@ -12,7 +12,7 @@ askUserKeepFile() {
 	while true; do
 		echo "Shall '$1' be deleted? [Y/n]: "
 		read answer
-		case "$answer" in
+		case "$answer" in 
 			n*|N*)
 				echo "Ok. We will keep that file. Keep in mind that you may need to alter it if any changes are done upstream. Your answer is saved for the future."
 				return 1
@@ -45,7 +45,7 @@ if [ "$1" = "configure" ]; then
 		echo "This is probably due to a previous manual installation."
 		echo "You probably want to delete this file now. Your evcc configuration stays untouched!"
 		askUserKeepFile "$ETC_SERVICE" || KEEP_ETC_SERVICE=$?
-	fi
+	fi	
 	if [ -f "$USR_LOCAL_BIN" ] && [ "$KEEP_USR_LOCAL_BIN" -eq 0 ]; then
 		echo "An alternate evcc binary was detected under '$USR_LOCAL_BIN'."
 		echo "This is probably due to a previous manual installation."
@@ -53,7 +53,7 @@ if [ "$1" = "configure" ]; then
 		askUserKeepFile "$USR_LOCAL_BIN" || KEEP_USR_LOCAL_BIN=$?
 	fi
 	# Save the user decision
-	cat > "$USER_CHOICE_CONFIG" <<EOF
+	cat > "$USER_CHOICE_CONFIG" <<EOF 
 #!/bin/sh
 KEEP_ETC_SERVICE=$KEEP_ETC_SERVICE
 KEEP_USR_LOCAL_BIN=$KEEP_USR_LOCAL_BIN
diff --git a/packaging/scripts/preinstall.sh b/packaging/scripts/preinstall.sh
index 30aab231..1da63b77 100644
--- a/packaging/scripts/preinstall.sh
+++ b/packaging/scripts/preinstall.sh
@@ -10,7 +10,7 @@ set -e
 EVCC_USER=evcc
 EVCC_GROUP=evcc
 EVCC_HOME="/var/lib/$EVCC_USER"
-RESTART_FLAG_FILE="/tmp/.restartEvccOnUpgrade"
+RESTART_FLAG_FILE=$EVCC_HOME/.restartOnUpgrade
 
 copyDbToUserDir() {
   CURRENT_USER=$(systemctl show -pUser evcc | cut -d= -f2)
diff --git a/server/socket.go b/server/socket.go
index 1c83dcb9..466cd2e8 100644
--- a/server/socket.go
+++ b/server/socket.go
@@ -6,6 +6,7 @@ import (
 	"math"
 	"net/http"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/evcc-io/evcc/util"
@@ -68,6 +69,8 @@ func ServeWebsocket(hub *SocketHub, w http.ResponseWriter, r *http.Request) {
 // SocketHub maintains the set of active clients and broadcasts messages to the
 // clients.
 type SocketHub struct {
+	mu sync.RWMutex
+
 	// Registered clients.
 	clients map[*SocketClient]bool
 
@@ -92,11 +95,11 @@ func encode(v interface{}) (string, error) {
 	var s string
 	switch val := v.(type) {
 	case time.Time:
-		var b []byte
-		if !val.IsZero() {
-			b, _ = val.Round(time.Second).Local().MarshalText()
+		if val.IsZero() {
+			s = "null"
+		} else {
+			s = fmt.Sprintf(`"%s"`, val.Format(time.RFC3339))
 		}
-		s = fmt.Sprintf(`"%s"`, string(b))
 	case time.Duration:
 		// must be before stringer to convert to seconds instead of string
 		s = fmt.Sprintf("%d", int64(val.Seconds()))
@@ -109,6 +112,8 @@ func encode(v interface{}) (string, error) {
 	default:
 		if b, err := json.Marshal(v); err == nil {
 			s = string(b)
+		} else {
+			return "", err
 		}
 	}
 	return s, nil
@@ -133,7 +138,9 @@ func kv(p util.Param) string {
 }
 
 func (h *SocketHub) welcome(client *SocketClient, params []util.Param) {
+	h.mu.Lock()
 	h.clients[client] = true
+	h.mu.Unlock()
 
 	var msg strings.Builder
 	msg.WriteString("{")
@@ -153,6 +160,9 @@ func (h *SocketHub) welcome(client *SocketClient, params []util.Param) {
 }
 
 func (h *SocketHub) broadcast(p util.Param) {
+	h.mu.RLock()
+	defer h.mu.RUnlock()
+
 	if len(h.clients) > 0 {
 		msg := "{" + kv(p) + "}"
 
@@ -173,10 +183,12 @@ func (h *SocketHub) Run(in <-chan util.Param, cache *util.Cache) {
 		case client := <-h.register:
 			h.welcome(client, cache.All())
 		case client := <-h.unregister:
+			h.mu.Lock()
 			if _, ok := h.clients[client]; ok {
 				close(client.send)
 				delete(h.clients, client)
 			}
+			h.mu.Unlock()
 		case msg, ok := <-in:
 			if !ok {
 				return // break if channel closed


Test patch:
diff --git a/server/socket_test.go b/server/socket_test.go
index f6abef3f..ffe7368f 100644
--- a/server/socket_test.go
+++ b/server/socket_test.go
@@ -2,14 +2,18 @@ package server
 
 import (
 	"math"
-	"reflect"
 	"testing"
 	"time"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestEncode(t *testing.T) {
+	now := time.Now()
+
 	tc := []struct {
-		in, out interface{}
+		in  interface{}
+		out string
 	}{
 		{int64(1), "1"},
 		{math.NaN(), "null"},
@@ -17,19 +21,13 @@ func TestEncode(t *testing.T) {
 		{"1.2345", "\"1.2345\""},
 		{time.Hour, "3600"},
 		{"minpv", "\"minpv\""},
+		{time.Time{}, "null"},
+		{now, "\"" + now.Format(time.RFC3339) + "\""},
 	}
 
 	for _, tc := range tc {
-		t.Logf("%+v", tc)
 		out, err := encode(tc.in)
-		if err != nil {
-			t.Error(err)
-		}
-
-		if out != tc.out {
-			t.Errorf("expected %v (string), got %v (%s)",
-				tc.out, out, reflect.TypeOf(out).Kind(),
-			)
-		}
+		assert.NoError(t, err)
+		assert.Equal(t, tc.out, out)
 	}
 }