Repository: evcc-io/evcc
Pull request #9995 (2023-09-21 18:21:00)

Easee: charger stuck in enabled state after vehicle disconnect
follow up fix for #9866


Patch:
diff --git a/charger/easee.go b/charger/easee.go
index 82803b98..971fc012 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -381,6 +381,12 @@ func (c *Easee) Enable(enable bool) (err error) {
 	opMode := c.opMode
 	c.mux.Unlock()
 
+	defer func() {
+		if err == nil {
+			c.enabled = enable
+		}
+	}()
+
 	// enable charger once if it's switched off
 	if enablingRequired {
 		data := easee.ChargerSettings{
@@ -412,12 +418,6 @@ func (c *Easee) Enable(enable bool) (err error) {
 		targetCurrent = 32
 	}
 
-	defer func() {
-		if err == nil {
-			c.enabled = enable
-		}
-	}()
-
 	uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action)
 	if _, err := c.postJSONAndWait(uri, nil); err != nil {
 		return err
diff --git a/charger/phoenix-ev-eth.go b/charger/phoenix-ev-eth.go
index 42a7e734..92155855 100644
--- a/charger/phoenix-ev-eth.go
+++ b/charger/phoenix-ev-eth.go
@@ -229,7 +229,7 @@ func (wb *PhoenixEVEth) getPhases(reg uint16) (float64, float64, float64, error)
 
 	var res [3]float64
 	for i := 0; i < 3; i++ {
-		res[i] = float64(encoding.Int32LswFirst(b[2*i:]))
+		res[i] = float64(encoding.Int32LswFirst(b[4*i:]))
 	}
 
 	return res[0], res[1], res[2], nil
diff --git a/i18n/bg.toml b/i18n/bg.toml
index 2598cac5..4049e442 100644
--- a/i18n/bg.toml
+++ b/i18n/bg.toml
@@ -1,6 +1,6 @@
 [batterySettings]
 batteryLevel = "Батерия %"
-capacity = "{energy} от{total}"
+capacity = "{energy} от {total}"
 legendBottomName = "приоритизиране на дом"
 legendBottomSubline = "не се използва за зареждане"
 legendMiddleName = "приоритизиране на кола"
@@ -18,13 +18,47 @@ low = "при донякъде заредена батерия"
 medium = "при наполовина заредена батерия"
 never = "само при наличие на излишък"
 
+[config]
+
+[config.form]
+example = "Пример"
+optional = "незадължителен"
+
+[config.main]
+addVehicle = "Добави автомобил"
+edit = "редактирай"
+title = "Конфигурация"
+vehicles = "Моите Автомобили"
+
+[config.validation]
+failed = "неуспешен"
+label = "Състояние"
+running = "проверка..."
+success = "успешна проверка"
+unknown = "неизвестен"
+validate = "провери"
+
+[config.vehicle]
+cancel = "Отмени"
+delete = "Изтрий автомобил"
+generic = "Други интеграции"
+offline = "Автомобил без свързаност"
+online = "Автомобил с онлайн свързаност"
+save = "Запази"
+scooter = "Скутер"
+template = "Производител"
+titleAdd = "Добави Автомобил"
+titleEdit = "Редактирай Автомобил"
+validateSave = "Провери и Запази"
+
 [footer]
 
 [footer.community]
-greenEnergySub1 = "заредено с помощтта на evcc"
+greenEnergy = "Слънчева енергия"
+greenEnergySub1 = "заредено с помощта на evcc"
 greenEnergySub2 = "от октомври 2022"
 greenShare = "дял на соларна енергия"
-greenShareSub1 = "мощност доставена от"
+greenShareSub1 = "мощност предоставена от"
 greenShareSub2 = "енергия от слънцето и батерията"
 power = "Мощност на зареждане"
 powerSub1 = "{activeClients} от {totalClients} участници"
@@ -42,3 +76,38 @@ priceFeedIn = "{feedInPrice} feed-in"
 priceGrid = "{gridPrice} мрежа"
 priceTitle = "Цена"
 savingsComparedToGrid = "сравнено с мрежата"
+savingsTitle = "Спестявате"
+savingsTotalEnergy = "{total} kWh заредени"
+since = "от {since}"
+tabTitle = "Моите данни"
+
+[footer.sponsor]
+becomeSponsor = "Станете Спонсор"
+confetti = "Готови ли сте за конфети?"
+confettiPromise = "Получавате стикери и дигитални конфети"
+sticker = "... или evcc стикери?"
+supportUs = "Нашата мисия е да направим слънчевата енергия масова. Подкрепете evcc със сума по ваша преценка."
+thanks = "Благодарим Ви, {sponsor}! С ваша помощ продължаваме да развиваме evcc."
+titleNoSponsor = "Подкрепете ни"
+titleSponsor = "Вие сте спонсор"
+
+[footer.telemetry]
+optIn = "Искам да споделям моите данни."
+optInMoreDetails = "Повече информация {0}."
+optInMoreDetailsLink = "тук"
+optInSponsorship = "Изисква спонсорство."
+
+[footer.version]
+availableLong = "налична е нова версия"
+modalCancel = "Откажи"
+modalDownload = "Свали"
+modalInstalledVersion = "Инсталирана версия"
+modalNoReleaseNotes = "Няма бележки за версията. Повече информация за новата версия:"
+modalTitle = "Налична е нова версия"
+modalUpdate = "Инсталирай"
+modalUpdateNow = "Инсталирай сега"
+modalUpdateStarted = "Стартирай новата версия на evcc..."
+modalUpdateStatusStart = "Инсталацията започна:"
+
+[gridSettings]
+activeHours = "{charging} от {total}"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 3559bd71..e15f6a73 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -25,6 +25,7 @@ example = "Exemple"
 optional = "optionnel"
 
 [config.main]
+addVehicle = "Ajouter un véhicule"
 edit = "éditer"
 title = "Configuration"
 vehicles = "Mes véhicules"
@@ -32,20 +33,22 @@ vehicles = "Mes véhicules"
 [config.validation]
 failed = "échec"
 label = "Statut"
-running = "en cours de validation..."
-success = "succès"
+running = "validation..."
+success = "réussi"
 unknown = "inconnu"
 validate = "valider"
 
 [config.vehicle]
 cancel = "Annuler"
+delete = "Supprimer un véhicule"
 generic = "Autres intégrations"
 offline = "Véhicule générique"
 online = "Véhicules avec API en ligne"
 save = "Enregistrer"
-scooter = "Trottinette"
+scooter = "Scooter"
 template = "Fabricant"
-titleEdit = "Editer le véhicule"
+titleAdd = "Ajouter un véhicule"
+titleEdit = "Modifier le véhicule"
 validateSave = "Valider & enregistrer"
 
 [footer]
@@ -234,7 +237,7 @@ descriptionEnergy = "Jusqu'à quand le véhicule doit-il être chargé à {targe
 descriptionSoc = "Quand le véhicule doit-il être chargé à {targetSoc}%?"
 inactiveLabel = "Temps visé"
 modalTitle = "Définir l'heure cible"
-onlyInPvMode = "La planification de charge ne fonctionne qu'en mode solaire."
+onlyInPvMode = "Le plan de recharge ne fonctionne qu’en mode solaire."
 planDuration = "Durée de charge"
 planPeriodLabel = "Période"
 planPeriodValue = "{start} à {end}"
@@ -242,7 +245,7 @@ planUnknown = "encore inconnu"
 priceLimit = "limite de prix : {price}"
 remove = "Ôter"
 setTargetTime = "aucun"
-targetEnergyRequired = "Vous devez aussi régler une limite d'énergie."
+targetEnergyRequired = "Vous devez également fixer une limite énergétique."
 targetIsInThePast = "Choisis une période dans le futur, Marty."
 targetIsTooFarInTheFuture = "Nous ajusterons le plan dès qu'on en saura plus sur le futur."
 title = "Temps visé"
@@ -320,7 +323,7 @@ meterstart = "Compteur début"
 meterstop = "Compteur fin"
 odometer = "Kilométrage"
 price = "Prix"
-started = "Début"
+started = "Commencé"
 title = "Session de charge"
 
 [sessions]
@@ -344,7 +347,7 @@ vehicle = "Véhicule"
 
 [sessions.csv]
 chargedenergy = "Énergie (kWh)"
-created = "Début"
+created = "Créé"
 finished = "Fin"
 identifier = "Identifiant"
 loadpoint = "Point de chargement"
diff --git a/server/socket_helper.go b/server/socket_helper.go
index f1a8c2e9..0cb3c6c3 100644
--- a/server/socket_helper.go
+++ b/server/socket_helper.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"math"
+	"reflect"
 	"strings"
 	"time"
 
@@ -39,8 +40,33 @@ func encode(v interface{}) (string, error) {
 	return s, nil
 }
 
+func encodeSlice(v interface{}) (string, error) {
+	rv := reflect.ValueOf(v)
+	res := make([]string, rv.Len())
+
+	for i := 0; i < rv.Len(); i++ {
+		var err error
+		if res[i], err = encode(rv.Index(i).Interface()); err != nil {
+			return "", err
+		}
+	}
+
+	return fmt.Sprintf("[%s]", strings.Join(res, ",")), nil
+}
+
 func kv(p util.Param) string {
-	val, err := encode(p.Val)
+	var (
+		val string
+		err error
+	)
+
+	// unwrap slices
+	if p.Val != nil && reflect.TypeOf(p.Val).Kind() == reflect.Slice {
+		val, err = encodeSlice(p.Val)
+	} else {
+		val, err = encode(p.Val)
+	}
+
 	if err != nil {
 		panic(err)
 	}
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index f31c73df..d6556e0a 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -331,9 +331,15 @@ presets:
       - name: integrateddevice
         type: bool
         advanced: true
-        description:
+        help:
           de: Integriertes Gerät (keine Fahrzeugverbindung)
           en: Integrated device (no vehicle connection)
+      - name: heating
+        type: bool
+        advanced: true
+        help:
+          de: Wärmeerzeuger (°C anstatt %)
+          en: Heating device (°C instead of %)
       - name: icon
         advanced: true
   ocpp:
diff --git a/util/templates/includes/switchsocket.tpl b/util/templates/includes/switchsocket.tpl
index 7f341943..e3776638 100644
--- a/util/templates/includes/switchsocket.tpl
+++ b/util/templates/includes/switchsocket.tpl
@@ -1,5 +1,11 @@
 {{ define "switchsocket" }}
 standbypower: {{ .standbypower }}
-{{ if .integrateddevice }}features: ["integrateddevice"]{{ end }}
+features:
+{{- if .integrateddevice }}
+- integrateddevice
+{{- end }}
+{{- if .heating }}
+- heating
+{{- end }}
 {{ if .icon }}icon: {{ .icon }}{{ end }}
 {{ end -}}


Test patch:
diff --git a/server/socket_test.go b/server/socket_test.go
index ffe7368f..dd40b945 100644
--- a/server/socket_test.go
+++ b/server/socket_test.go
@@ -31,3 +31,20 @@ func TestEncode(t *testing.T) {
 		assert.Equal(t, tc.out, out)
 	}
 }
+
+func TestEncodeSlice(t *testing.T) {
+	tc := []struct {
+		in  interface{}
+		out string
+	}{
+		{[]string{"a", "b"}, `["a","b"]`},
+		{[2]int64{1, 2}, `[1,2]`},
+		{[]float64{1, math.NaN()}, `[1,null]`},
+	}
+
+	for _, tc := range tc {
+		out, err := encodeSlice(tc.in)
+		assert.NoError(t, err)
+		assert.Equal(t, tc.out, out)
+	}
+}