Repository: evcc-io/evcc
Pull request #1966 (2021-12-01 13:04:37)

cli configure updates
- Add `--advanced` mode, where params defined as `advanced: true` will be asked
- Add support for handling user input for lists, e.g. `identifiers`
- Bool values are no text inputs but No/Yes selections
- Common definitions (params and render) are moved into single `parambaselist.yaml` file
- Some refactoring


andig: > Always render all params, even if the user did not provide any input

Finde ich nicht gut- damit geht die Einfachheit der Konfiguration verloren
DerAndereAndi: > Finde ich nicht gut- damit geht die Einfachheit der Konfiguration verloren

Ist wieder drin
andig: Was ist denn die Ursache für den 400er Easee Error den es vorher nixht gab?
DerAndereAndi: das Easee backend? Der kam nicht wegen den Templates, und kam auch nicht bei jedem Testlauf, sondern etwa bei 1 von 10-15.

Patch:
diff --git a/README.md b/README.md
index c31867ab..dc5c0398 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 - simple and clean user interface
 - multiple [chargers](#charger):
   - Open source: [openWB](https://openwb.de/), [EVSEWifi](https://www.evse-wifi.de) (includes smartWB)
-  - Closed source: ABL eMH1, cFos PowerBrain, go-eCharger, Heidelberg Energy Control, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect, EEBUS (experimental)
+  - Closed source: ABL eMH1, Alfen Eve, cFos PowerBrain, go-eCharger, Heidelberg Energy Control, KEBA/BMW, NRGkick, Wallbe, Mobile Charger Connect, EEBUS (experimental)
   - Build-your-own: Phoenix (includes ESL Walli), [SimpleEVSE](https://www.evse-wifi.de/produkt-schlagwort/simple-evse-wb/)
   - Smart-Home outlets: FritzDECT, Shelly, Tasmota, TP-Link
 - multiple [meters](#meter): ModBus (Eastron SDM, MPM3PM, SBC ALE3 and many more), Discovergy (using HTTP plugin), SMA Sunny Home Manager and Energy Meter, KOSTAL Smart Energy Meter (KSEM, EMxx), any Sunspec-compatible inverter or home battery devices (Fronius, SMA, SolarEdge, KOSTAL, STECA, E3DC, ...), Tesla PowerWall, LG ESS HOME
@@ -29,7 +29,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
 
 ## Index <!-- omit in toc -->
 
-- [Deutsche Dokumentation](#dokumentation)
+- [Dokumentation](#dokumentation)
 - [Getting started](#getting-started)
 - [Installation](#installation)
 - [Configuration](#configuration)
@@ -192,6 +192,7 @@ Charger is responsible for handling EV state and adjusting charge current.
 Available charger implementations are:
 
 - `abl`: ABL eMH1 (requires Modbus adapter; [sponsors only](#sponsorship))
+- `alfen`: Alfen Eve (Single and Double, NG platform, [sponsors only](#sponsorship))
 - `cfos`: cFos PowerBrain charger (meters must configured separately, [sponsors only](#sponsorship))
 - `easee`: Easee Home charger ([sponsors only](#sponsorship))
 - `eebus`: EEBUS compatible chargers (experimental)
@@ -361,7 +362,7 @@ messaging:
     [...]
 ```
 
-#### Notification Events
+#### Notification Events <!-- omit in toc -->
 
 The available events are:
 
@@ -378,7 +379,7 @@ Configuration is done according to the scheme of following example for the `star
       msg: Started charging in "${mode}" mode
 ```
 
-#### Notification Services
+#### Notification Services <!-- omit in toc -->
 
 Following types of notification services can be configured:
 
diff --git a/charger/alfen.go b/charger/alfen.go
new file mode 100644
index 00000000..24613791
--- /dev/null
+++ b/charger/alfen.go
@@ -0,0 +1,199 @@
+package charger
+
+// LICENSE
+
+// Copyright (c) 2019-2021 andig
+
+// This module is NOT covered by the MIT license. All rights reserved.
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import (
+	"encoding/binary"
+	"fmt"
+	"math"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+	"github.com/volkszaehler/mbmd/meters/rs485"
+)
+
+// Alfen charger implementation
+type Alfen struct {
+	conn *modbus.Connection
+	curr float64
+}
+
+const (
+	alfenRegPower      = 344
+	alfenRegEnergy     = 374  // 390
+	alfenRegStatus     = 1201 // 5 registers
+	alfenRegAmpsConfig = 1210
+	alfenRegPhases     = 1215
+)
+
+var alfenRegCurrents = []uint16{320, 322, 324}
+
+func init() {
+	registry.Add("alfen", NewAlfenFromConfig)
+}
+
+// NewAlfenFromConfig creates a Alfen charger from generic config
+func NewAlfenFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.Settings{
+		ID: 1,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewAlfen(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID)
+}
+
+// NewAlfen creates Alfen charger
+func NewAlfen(uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, modbus.TcpFormat, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("alfen")
+	conn.Logger(log.TRACE)
+
+	wb := &Alfen{
+		conn: conn,
+		curr: 6,
+	}
+
+	return wb, err
+}
+
+// Status implements the api.Charger interface
+func (wb *Alfen) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadHoldingRegisters(alfenRegStatus, 5)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	switch r := rune(b[0]); r {
+	case 'A', 'B', 'C', 'D', 'E', 'F':
+		return api.ChargeStatus(r), nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %0x", b[:1])
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Alfen) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(alfenRegAmpsConfig, 2)
+	if err != nil {
+		return false, err
+	}
+
+	return math.Float32frombits(binary.BigEndian.Uint32(b)) > 0, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Alfen) Enable(enable bool) error {
+	var curr float64
+	if enable {
+		curr = wb.curr
+	}
+
+	return wb.MaxCurrentMillis(curr)
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Alfen) MaxCurrent(current int64) error {
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*Alfen)(nil)
+
+// MaxCurrent implements the api.ChargerEx interface
+func (wb *Alfen) MaxCurrentMillis(current float64) error {
+	b := make([]byte, 4)
+	binary.BigEndian.PutUint32(b, math.Float32bits(float32(current)))
+
+	_, err := wb.conn.WriteMultipleRegisters(alfenRegAmpsConfig, 2, b)
+	if err == nil {
+		wb.curr = current
+	}
+
+	return err
+}
+
+var _ api.Meter = (*Alfen)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *Alfen) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(alfenRegPower, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return rs485.RTUIeee754ToFloat64(b), err
+}
+
+var _ api.MeterEnergy = (*Alfen)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (wb *Alfen) TotalEnergy() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(alfenRegEnergy, 4)
+	if err != nil {
+		return 0, err
+	}
+
+	return math.Float64frombits(binary.BigEndian.Uint64(b)) / 1e3, err
+}
+
+var _ api.MeterCurrent = (*Alfen)(nil)
+
+// Currents implements the api.MeterCurrent interface
+func (wb *Alfen) Currents() (float64, float64, float64, error) {
+	var currents []float64
+	for _, reg := range alfenRegCurrents {
+		b, err := wb.conn.ReadHoldingRegisters(reg, 2)
+		if err != nil {
+			return 0, 0, 0, err
+		}
+
+		currents = append(currents, rs485.RTUIeee754ToFloat64(b))
+	}
+
+	return currents[0], currents[1], currents[2], nil
+}
+
+var _ api.ChargePhases = (*Alfen)(nil)
+
+// Phases1p3p implements the api.ChargePhases interface
+func (c *Alfen) Phases1p3p(phases int) error {
+	_, err := c.conn.WriteSingleRegister(alfenRegPhases, uint16(phases))
+	return err
+}
+
+// var _ api.Diagnosis = (*Alfen)(nil)
+
+// // Diagnose implements the api.Diagnosis interface
+// func (wb *Alfen) Diagnose() {
+// 	b, err := wb.conn.ReadHoldingRegisters(ablRegFirmware, 2)
+// 	if err == nil {
+// 		fmt.Printf("Firmware: %0 x\n", b)
+// 	}
+// }
diff --git a/cmd/configure.go b/cmd/configure.go
index 3fbc4f1a..0ea890ac 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -19,6 +19,7 @@ var configureCmd = &cobra.Command{
 func init() {
 	rootCmd.AddCommand(configureCmd)
 	configureCmd.Flags().String("lang", "", "Define the localization to be used (en, de)")
+	configureCmd.Flags().Bool("advanced", false, "Enables handling of advanced configuration options")
 	configureCmd.Flags().Bool("expand", false, "Enables rendering expanded configuration files")
 }
 
@@ -30,6 +31,11 @@ func runConfigure(cmd *cobra.Command, args []string) {
 		log.FATAL.Fatal(err)
 	}
 
+	advanced, err := cmd.Flags().GetBool("advanced")
+	if err != nil {
+		panic(err)
+	}
+
 	expand, err := cmd.Flags().GetBool("expand")
 	if err != nil {
 		panic(err)
@@ -37,5 +43,5 @@ func runConfigure(cmd *cobra.Command, args []string) {
 
 	util.LogLevel(viper.GetString("log"), nil)
 
-	impl.Run(log, lang, expand)
+	impl.Run(log, lang, advanced, expand)
 }
diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go
index 2d1dbcd8..990ee598 100644
--- a/cmd/configure/configure.go
+++ b/cmd/configure/configure.go
@@ -11,7 +11,6 @@ import (
 type device struct {
 	Name            string
 	Title           string
-	LogLevel        string
 	Yaml            string
 	ChargerHasMeter bool // only used with chargers to detect if we need to ask for a charge meter
 }
diff --git a/cmd/configure/flow.go b/cmd/configure/flow.go
index ee1f693a..0737d4f8 100644
--- a/cmd/configure/flow.go
+++ b/cmd/configure/flow.go
@@ -53,7 +53,7 @@ func (c *CmdConfigure) configureDeviceGuidedSetup() {
 		// we only ask for the configuration for the first usage
 		deviceCategory = supportedDeviceCategories[0]
 
-		values = c.processConfig(templateItem.Params, deviceCategory, false)
+		values = c.processConfig(templateItem.Params, deviceCategory)
 
 		deviceItem, err = c.processDeviceValues(values, templateItem, deviceItem, deviceCategory)
 		if err != nil {
@@ -120,7 +120,7 @@ func (c *CmdConfigure) configureLinkedTypes(templateItem templates.Template) {
 				break
 			}
 
-			values := c.processConfig(linkedTemplateItem.Params, category, false)
+			values := c.processConfig(linkedTemplateItem.Params, category)
 			deviceItem, err = c.processDeviceValues(values, linkedTemplateItem, deviceItem, category)
 			if err != nil {
 				if !errors.Is(err, c.errDeviceNotValid) {
@@ -164,7 +164,7 @@ func (c *CmdConfigure) configureDeviceCategory(deviceCategory DeviceCategory) (d
 		}
 
 		deviceDescription = templateItem.Description
-		values := c.processConfig(templateItem.Params, deviceCategory, false)
+		values := c.processConfig(templateItem.Params, deviceCategory)
 
 		device, err = c.processDeviceValues(values, templateItem, device, deviceCategory)
 		if err != nil {
diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go
index 90a973ae..74eb6e30 100644
--- a/cmd/configure/helper.go
+++ b/cmd/configure/helper.go
@@ -32,7 +32,6 @@ func (c *CmdConfigure) processDeviceValues(values map[string]interface{}, templa
 
 	device.Name = fmt.Sprintf("%s%d", DeviceCategories[deviceCategory].defaultName, c.addedDeviceIndex)
 	device.Title = templateItem.Description
-	device.LogLevel = templateItem.LogLevel
 	for item, value := range values {
 		if strings.ToLower(item) != "title" {
 			continue
@@ -233,120 +232,158 @@ func (c *CmdConfigure) paramChoiceValues(params []templates.Param, name string)
 // processConfig processes an EVCC configuration item
 // Returns:
 //   a map with param name and values
-func (c *CmdConfigure) processConfig(paramItems []templates.Param, deviceCategory DeviceCategory, includeAdvanced bool) map[string]interface{} {
+func (c *CmdConfigure) processConfig(paramItems []templates.Param, deviceCategory DeviceCategory) map[string]interface{} {
 	usageFilter := DeviceCategories[deviceCategory].categoryFilter
 
 	additionalConfig := make(map[string]interface{})
-	selectedModbusKey := ""
 
 	fmt.Println()
 	fmt.Println(c.localizedString("Config_Title", nil))
 	fmt.Println()
 
 	for _, param := range paramItems {
-		if param.Name == templates.ParamModbus {
-			// baudrate and comset defaults can be overwritten, as they are device specific
-			deviceDefaultBaudrate := templates.ModbusParamValueBaudrate
-			if param.Baudrate != 0 {
-				deviceDefaultBaudrate = param.Baudrate
-			}
-			deviceDefaultComset := templates.ModbusParamValueComset
-			if param.Comset != "" {
-				deviceDefaultBaudrate = param.Baudrate
-			}
-
-			choices := []string{}
-			choiceKeys := []string{}
-			for _, choice := range param.Choice {
-				switch choice {
-				case templates.ModbusChoiceRS485:
-					choices = append(choices, "Serial (USB-RS485 Adapter)")
-					choiceKeys = append(choiceKeys, templates.ModbusKeyRS485Serial)
-					choices = append(choices, "Serial (Ethernet-RS485 Adapter)")
-					choiceKeys = append(choiceKeys, templates.ModbusKeyRS485TCPIP)
-				case templates.ModbusChoiceTCPIP:
-					choices = append(choices, "TCP/IP")
-					choiceKeys = append(choiceKeys, templates.ModbusKeyTCPIP)
-				}
-			}
-
-			if len(choices) > 0 {
-				// ask for modbus address
-				id := c.askValue(question{
-					label:        "ID",
-					help:         "Modbus ID",
-					defaultValue: 1,
-					valueType:    templates.ParamValueTypeNumber,
-					required:     true})
-				additionalConfig[templates.ModbusParamNameId] = id
-
-				// ask for modbus interface type
-				index := 0
-				if len(choices) > 1 {
-					index, _ = c.askChoice(c.localizedString("Config_ModbusInterface", nil), choices)
-				}
-				selectedModbusKey = choiceKeys[index]
-				switch selectedModbusKey {
-				case templates.ModbusKeyRS485Serial:
-					device := c.askValue(question{
-						label:        "Device",
-						help:         "USB-RS485 Adapter Adresse",
-						exampleValue: templates.ModbusParamValueDevice,
-						required:     true})
-					additionalConfig[templates.ModbusParamNameDevice] = device
-
-					baudrate := c.askValue(question{
-						label:        "Baudrate",
-						defaultValue: deviceDefaultBaudrate,
-						valueType:    templates.ParamValueTypeNumber,
-						required:     true})
-					additionalConfig[templates.ModbusParamNameBaudrate] = baudrate
-
-					comset := c.askValue(question{
-						label:        "ComSet",
-						defaultValue: deviceDefaultComset,
-						required:     true})
-					additionalConfig[templates.ModbusParamNameComset] = comset
-
-				case templates.ModbusKeyRS485TCPIP, templates.ModbusKeyTCPIP:
-					if selectedModbusKey == templates.ModbusKeyRS485TCPIP {
-						additionalConfig[templates.ModbusParamNameRTU] = "true"
-					}
-					host := c.askValue(question{
-						label:        "Host",
-						exampleValue: templates.ModbusParamValueHost,
-						required:     true})
-					additionalConfig[templates.ModbusParamNameHost] = host
-
-					port := c.askValue(question{
-						label:        "Port",
-						defaultValue: templates.ModbusParamValuePort,
-						valueType:    templates.ParamValueTypeNumber,
-						required:     true})
-					additionalConfig[templates.ModbusParamNamePort] = port
-				}
+		switch param.Name {
+		case templates.ParamModbus:
+			c.processModbusConfig(param, deviceCategory, additionalConfig)
+		case templates.ParamUsage:
+			if usageFilter != "" {
+				additionalConfig[param.Name] = usageFilter.String()
 			}
-		} else if param.Name != templates.ParamUsage {
-			if !includeAdvanced && param.Advanced {
+		default:
+			if !c.advancedMode && param.Advanced {
 				continue
 			}
 
-			userFriendly := c.userFriendlyTexts(param)
-			value := c.askValue(question{
-				label:        userFriendly.Name,
-				defaultValue: userFriendly.Default,
-				exampleValue: userFriendly.Example,
-				help:         userFriendly.Help.String(c.lang),
-				valueType:    userFriendly.ValueType,
-				mask:         userFriendly.Mask,
-				required:     userFriendly.Required})
-			additionalConfig[param.Name] = value
-		} else if param.Name == templates.ParamUsage {
-			if usageFilter != "" {
-				additionalConfig[param.Name] = usageFilter.String()
+			switch param.ValueType {
+			case templates.ParamValueTypeStringList:
+				additionalConfig[param.Name] = c.processListInputConfig(param)
+			default:
+				additionalConfig[param.Name] = c.processInputConfig(param)
 			}
 		}
 	}
 
 	return additionalConfig
 }
+
+// handle user input of multiple items in a list
+func (c *CmdConfigure) processListInputConfig(param templates.Param) []string {
+	var values []string
+
+	// ask for values until the decides stops
+	for ok := true; ok; {
+		newValue := c.processInputConfig(param)
+		values = append(values, newValue)
+
+		if newValue == "" {
+			break
+		}
+
+		if !c.askYesNo("  " + c.localizedString("Config_AddAnotherValue", nil)) {
+			break
+		}
+	}
+
+	return values
+}
+
+// handle user input for a simple one value input
+func (c *CmdConfigure) processInputConfig(param templates.Param) string {
+	userFriendly := c.userFriendlyTexts(param)
+	return c.askValue(question{
+		label:        userFriendly.Name,
+		defaultValue: userFriendly.Default,
+		exampleValue: userFriendly.Example,
+		help:         userFriendly.Help.String(c.lang),
+		valueType:    userFriendly.ValueType,
+		mask:         userFriendly.Mask,
+		required:     userFriendly.Required})
+}
+
+// handle user input for a device modbus configuration
+func (c *CmdConfigure) processModbusConfig(param templates.Param, deviceCategory DeviceCategory, additionalConfig map[string]interface{}) {
+	var selectedModbusKey string
+
+	// baudrate and comset defaults can be overwritten, as they are device specific
+	deviceDefaultBaudrate := templates.ModbusParamValueBaudrate
+	if param.Baudrate != 0 {
+		deviceDefaultBaudrate = param.Baudrate
+	}
+	deviceDefaultComset := templates.ModbusParamValueComset
+	if param.Comset != "" {
+		deviceDefaultBaudrate = param.Baudrate
+	}
+
+	var choices []string
+	var choiceKeys []string
+
+	for _, choice := range param.Choice {
+		switch choice {
+		case templates.ModbusChoiceRS485:
+			choices = append(choices, "Serial (USB-RS485 Adapter)")
+			choiceKeys = append(choiceKeys, templates.ModbusKeyRS485Serial)
+			choices = append(choices, "Serial (Ethernet-RS485 Adapter)")
+			choiceKeys = append(choiceKeys, templates.ModbusKeyRS485TCPIP)
+		case templates.ModbusChoiceTCPIP:
+			choices = append(choices, "TCP/IP")
+			choiceKeys = append(choiceKeys, templates.ModbusKeyTCPIP)
+		}
+	}
+
+	if len(choices) > 0 {
+		// ask for modbus address
+		id := c.askValue(question{
+			label:        "ID",
+			help:         "Modbus ID",
+			defaultValue: 1,
+			valueType:    templates.ParamValueTypeNumber,
+			required:     true})
+		additionalConfig[templates.ModbusParamNameId] = id
+
+		// ask for modbus interface type
+		var index int
+		if len(choices) > 1 {
+			index, _ = c.askChoice(c.localizedString("Config_ModbusInterface", nil), choices)
+		}
+		selectedModbusKey = choiceKeys[index]
+		switch selectedModbusKey {
+		case templates.ModbusKeyRS485Serial:
+			device := c.askValue(question{
+				label:        "Device",
+				help:         "USB-RS485 Adapter Adresse",
+				exampleValue: templates.ModbusParamValueDevice,
+				required:     true})
+			additionalConfig[templates.ModbusParamNameDevice] = device
+
+			baudrate := c.askValue(question{
+				label:        "Baudrate",
+				defaultValue: deviceDefaultBaudrate,
+				valueType:    templates.ParamValueTypeNumber,
+				required:     true})
+			additionalConfig[templates.ModbusParamNameBaudrate] = baudrate
+
+			comset := c.askValue(question{
+				label:        "ComSet",
+				defaultValue: deviceDefaultComset,
+				required:     true})
+			additionalConfig[templates.ModbusParamNameComset] = comset
+
+		case templates.ModbusKeyRS485TCPIP, templates.ModbusKeyTCPIP:
+			if selectedModbusKey == templates.ModbusKeyRS485TCPIP {
+				additionalConfig[templates.ModbusParamNameRTU] = "true"
+			}
+			host := c.askValue(question{
+				label:        "Host",
+				exampleValue: templates.ModbusParamValueHost,
+				required:     true})
+			additionalConfig[templates.ModbusParamNameHost] = host
+
+			port := c.askValue(question{
+				label:        "Port",
+				defaultValue: templates.ModbusParamValuePort,
+				valueType:    templates.ParamValueTypeNumber,
+				required:     true})
+			additionalConfig[templates.ModbusParamNamePort] = port
+		}
+	}
+}
diff --git a/cmd/configure/localization/de.toml b/cmd/configure/localization/de.toml
index 1724fa40..6ad6d1e8 100644
--- a/cmd/configure/localization/de.toml
+++ b/cmd/configure/localization/de.toml
@@ -75,6 +75,9 @@ Requirements_EEBUS_Pairing = "Du hast eine Wallbox ausgewählt, welche über das
 
 Config_Title = "Führe folgende Einstellungen durch:"
 Config_ModbusInterface = "Wähle die ModBus Schnittstelle aus"
+Config_AddAnotherValue = "Möchtest du einen weiteren Wert hinzufügen?"
+Config_Yes = "Ja"
+Config_No = "Nein"
 
 Cancel = "Die Konfiguration wurde abgebrochen.\n\nFalls diese geführte Konfiguration für dich noch nicht funktioniert, versuche es doch mal mit der manuellen Konfiguration. Details findest du auf der folgenden Webseite: https://docs.evcc.io/docs/installation/configuration\n"
 InputError = "Bei der Eingabe ist ein Fehler aufgetreten:"
@@ -82,10 +85,8 @@ InputError = "Bei der Eingabe ist ein Fehler aufgetreten:"
 Value_Required = "erforderlich"
 Value_Optional = "optional"
 Value_Sample = "Beispiel"
-Value_Bool = "'true' für ja oder 'false' für nein"
 ValueError_Used = "Dieser Wert wird bereits verwendet."
 ValueError_Empty = "Der Wert darf nicht leer sein."
-ValueError_Bool = "Der Wert muss 'true' (für ja) oder 'false' (für nein) sein."
 ValueError_Float = "Der Wert muss eine Zahl sein. Nachkommastellen mit . anstatt mit , trennen."
 ValueError_Number = "Der Wert muss eine ganzzahlige Zahl sein."
 
diff --git a/cmd/configure/localization/en.toml b/cmd/configure/localization/en.toml
index a610f347..00bd7091 100644
--- a/cmd/configure/localization/en.toml
+++ b/cmd/configure/localization/en.toml
@@ -75,6 +75,9 @@ Requirements_EEBUS_Pairing = "You selected a wallbox, which will be access via t
 
 Config_Title = "Please provide the following settings:"
 Config_ModbusInterface = "Choose the ModBus interface"
+Config_AddAnotherValue = "Do you want to add another value?"
+Config_Yes = "Yes"
+Config_No = "No"
 
 Cancel = "The configuration was cancelled.\n\nIf this guided configuration process doesn't work for you yet, please try the manual configuration. You can find more details about that on our website: https://docs.evcc.io/docs/installation/configuration\n"
 InputError = "An input error occurred:"
@@ -82,10 +85,8 @@ InputError = "An input error occurred:"
 Value_Required = "required"
 Value_Optional = "optional"
 Value_Sample = "Example"
-Value_Bool = "'true' for yes or 'false' for no"
 ValueError_Used = "This value is aready in use."
 ValueError_Empty = "The value may not be empty."
-ValueError_Bool = "The value has to be 'true' (for yes) or 'false' (for no)."
 ValueError_Float = "The value has to be a number."
 ValueError_Number = "The value has to be an integer."
 
diff --git a/cmd/configure/main.go b/cmd/configure/main.go
index d0e60aa7..fb74cfec 100644
--- a/cmd/configure/main.go
+++ b/cmd/configure/main.go
@@ -29,14 +29,15 @@ type CmdConfigure struct {
 	log           *util.Logger
 
 	lang                                 string
-	expandedMode                         bool
+	advancedMode, expandedMode           bool
 	addedDeviceIndex                     int
 	errItemNotPresent, errDeviceNotValid error
 }
 
 // Run starts the interactive configuration
-func (c *CmdConfigure) Run(log *util.Logger, flagLang string, expandedMode bool) {
+func (c *CmdConfigure) Run(log *util.Logger, flagLang string, advancedMode, expandedMode bool) {
 	c.log = log
+	c.advancedMode = advancedMode
 	c.expandedMode = expandedMode
 
 	c.log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)
diff --git a/cmd/configure/survey.go b/cmd/configure/survey.go
index e8c1e4ed..941dd420 100644
--- a/cmd/configure/survey.go
+++ b/cmd/configure/survey.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"os"
 	"strconv"
-	"strings"
 
 	"github.com/AlecAivazis/survey/v2"
 	"github.com/AlecAivazis/survey/v2/terminal"
@@ -118,8 +117,26 @@ type question struct {
 	mask, required             bool
 }
 
+// askBoolValue asks for a boolean value selection for a given question
+func (c *CmdConfigure) askBoolValue(label string) string {
+	choices := []string{c.localizedString("Config_No", nil), c.localizedString("Config_Yes", nil)}
+	values := []string{"false", "true"}
+
+	index, _ := c.askChoice(label, choices)
+	return values[index]
+}
+
 // askValue asks for value input for a given question (template param)
 func (c *CmdConfigure) askValue(q question) string {
+	if q.valueType == templates.ParamValueTypeBool {
+		label := q.label
+		if q.help != "" {
+			label = q.help
+		}
+
+		return c.askBoolValue(label)
+	}
+
 	input := ""
 
 	var err error
@@ -134,12 +151,6 @@ func (c *CmdConfigure) askValue(q question) string {
 			return errors.New(c.localizedString("ValueError_Empty", nil))
 		}
 
-		if q.valueType == templates.ParamValueTypeBool {
-			if strings.ToLower(value) != "true" && strings.ToLower(value) != "false" {
-				return errors.New(c.localizedString("ValueError_Bool", nil))
-			}
-		}
-
 		if q.valueType == templates.ParamValueTypeFloat {
 			_, err := strconv.ParseFloat(value, 64)
 			if err != nil {
@@ -166,9 +177,6 @@ func (c *CmdConfigure) askValue(q question) string {
 	if q.exampleValue != "" {
 		help += fmt.Sprintf(" ("+c.localizedString("Value_Sample", nil)+": %s)", q.exampleValue)
 	}
-	if q.valueType == templates.ParamValueTypeBool {
-		help += " (" + c.localizedString("Value_Bool", nil) + ")"
-	}
 
 	if q.mask {
 		prompt := &survey.Password{
diff --git a/cmd/configure/texts.go b/cmd/configure/texts.go
index 9d4188d8..ba373fa4 100644
--- a/cmd/configure/texts.go
+++ b/cmd/configure/texts.go
@@ -81,7 +81,7 @@ func (c *CmdConfigure) userFriendlyTexts(param templates.Param) templates.Param
 		if result.Help.String(c.lang) == "" {
 			result.Help.SetString(c.lang, c.localizedString("UserFriendly_Vin_Help", nil))
 		}
-	case "identifier":
+	case "identifiers":
 		result.Name = c.localizedString("UserFriendly_Identifier_Name", nil)
 		if result.Help.String(c.lang) == "" {
 			result.Help.SetString(c.lang, c.localizedString("UserFriendly_Identifier_Help", nil))
diff --git a/meter/sma.go b/meter/sma.go
index 98b59c18..48dea3a2 100644
--- a/meter/sma.go
+++ b/meter/sma.go
@@ -15,7 +15,6 @@ import (
 
 // SMA supporting SMA Home Manager 2.0, SMA Energy Meter 30 and SMA inverter
 type SMA struct {
-	log    *util.Logger
 	uri    string
 	scale  float64
 	device *sma.Device
@@ -48,7 +47,6 @@ func NewSMAFromConfig(other map[string]interface{}) (api.Meter, error) {
 // NewSMA creates a SMA Meter
 func NewSMA(uri, password, iface string, serial uint32, scale float64) (api.Meter, error) {
 	sm := &SMA{
-		log:   util.NewLogger("sma").Redact(password),
 		uri:   uri,
 		scale: scale,
 	}
@@ -74,6 +72,12 @@ func NewSMA(uri, password, iface string, serial uint32, scale float64) (api.Mete
 	default:
 		return nil, errors.New("missing uri or serial")
 	}
+
+	// call UpdateValues first to check if we get an error
+	if err := sm.device.UpdateValues(); err != nil {
+		return nil, err
+	}
+
 	// start update loop manually to get values as fast as possible
 	sm.device.StartUpdateLoop()
 
diff --git a/provider/sma/device.go b/provider/sma/device.go
index 9ccee0b0..d1a658f9 100644
--- a/provider/sma/device.go
+++ b/provider/sma/device.go
@@ -24,15 +24,16 @@ type Device struct {
 func (d *Device) StartUpdateLoop() {
 	d.once.Do(func() {
 		go func() {
-			d.updateValues()
 			for range time.NewTicker(time.Second * 5).C {
-				d.updateValues()
+				if err := d.UpdateValues(); err != nil {
+					d.log.ERROR.Println(err)
+				}
 			}
 		}()
 	})
 }
 
-func (d *Device) updateValues() {
+func (d *Device) UpdateValues() error {
 	d.mux.Lock()
 	defer d.mux.Unlock()
 
@@ -42,9 +43,7 @@ func (d *Device) updateValues() {
 		d.mux.Update()
 	}
 
-	if err != nil {
-		d.log.ERROR.Println(err)
-	}
+	return err
 }
 
 func (d *Device) Values() (map[sunny.ValueID]interface{}, error) {
diff --git a/templates/README.md b/templates/README.md
index 5191eddb..6f9044ce 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -112,9 +112,14 @@ Allows to define a list of meter devices that are typically installed with this
 **Possible values**:
 
 - `string`: for string values (default)
-- `bool`: for `true` and `false` values
+- `bool`: for `true` and `false` values. If `help` is provided, than that help text is presented as the question
 - `number`: for int values
 - `float`: for float values
+- `stringlist`: for a list of strings, e.g.used for defining a list of `identifiers` for `vehicles`
+
+### `advanced`
+
+`advanced` allows to specify if the param should only be asked if the cli is run with `--advanced`. Mostly used for non required params that are meant for users with advanced needs and knowledge.
 
 ### `help`
 
diff --git a/templates/definition/charger/porsche-pmcc.yaml b/templates/definition/charger/porsche-pmcc.yaml
index 2bde1123..a4845422 100644
--- a/templates/definition/charger/porsche-pmcc.yaml
+++ b/templates/definition/charger/porsche-pmcc.yaml
@@ -1,6 +1,5 @@
 template: pmcc
 description: Porsche Mobile Charger Connect
-loglevel: eebus
 requirements:
   eebus: true
 params:
diff --git a/templates/definition/charger/porsche-pmcp.yaml b/templates/definition/charger/porsche-pmcp.yaml
index e98d570c..3ea1482d 100644
--- a/templates/definition/charger/porsche-pmcp.yaml
+++ b/templates/definition/charger/porsche-pmcp.yaml
@@ -1,6 +1,5 @@
 template: pmcp
 description: Porsche Mobile Charger Plus
-loglevel: eebus
 requirements:
   eebus: true
 params:
diff --git a/templates/definition/embed.go b/templates/definition/embed.go
index 2208d5b0..248da7d7 100644
--- a/templates/definition/embed.go
+++ b/templates/definition/embed.go
@@ -5,4 +5,7 @@ import "embed"
 var (
 	//go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml
 	YamlTemplates embed.FS
+
+	//go:embed parambaselist.yaml
+	ParamBaseListDefinition string
 )
diff --git a/templates/definition/meter/sma-homemanager.yaml b/templates/definition/meter/sma-homemanager.yaml
index 62d24515..a3559627 100644
--- a/templates/definition/meter/sma-homemanager.yaml
+++ b/templates/definition/meter/sma-homemanager.yaml
@@ -2,7 +2,6 @@ template: sma-home-manager
 description: SMA Sunny Home Manager 2.0
 requirements:
   hems: sma
-loglevel: sma
 guidedsetup:
   enable: true
   linked: 
diff --git a/templates/definition/parambaselist.yaml b/templates/definition/parambaselist.yaml
new file mode 100644
index 00000000..7f8ab854
--- /dev/null
+++ b/templates/definition/parambaselist.yaml
@@ -0,0 +1,36 @@
+vehicle:
+  params:
+  - name: title
+  - name: user
+    required: true
+  - name: password
+    required: true
+    mask: true
+  - name: vin
+    example: W...
+  - name: capacity
+    default: '50'
+    valueType: float
+  - name: identifiers
+    advanced: true
+    valueType: stringlist
+  render: |
+    {{define "vehicle-common"}}
+    {{- if ne .title "" }}
+    title: {{ .title }}
+    {{- end }}
+    user: {{ .user }}
+    password: {{ .password }}
+    {{- if ne .capacity "" }}
+    capacity: {{ .capacity }}
+    {{- end }}
+    {{- if ne .vin "" }}
+    vin: {{ .vin }}
+    {{- end }}
+    {{- if ne (len .identifiers) 0 }}
+    identifiers:
+    {{-   range .identifiers }}
+    - {{ . }}
+    {{-   end }}
+    {{- end }}
+    {{end}}
diff --git a/templates/definition/vehicle/audi.yaml b/templates/definition/vehicle/audi.yaml
index f1319797..3853d27f 100644
--- a/templates/definition/vehicle/audi.yaml
+++ b/templates/definition/vehicle/audi.yaml
@@ -8,12 +8,4 @@ params:
   default: 60
 render: |
   type: audi
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml
index e08188a5..d269abf2 100644
--- a/templates/definition/vehicle/bmw.yaml
+++ b/templates/definition/vehicle/bmw.yaml
@@ -8,12 +8,4 @@ params:
   default: 65
 render: |
   type: bmw
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/carwings.yaml b/templates/definition/vehicle/carwings.yaml
index 9fe51fd2..364b5d88 100644
--- a/templates/definition/vehicle/carwings.yaml
+++ b/templates/definition/vehicle/carwings.yaml
@@ -6,12 +6,4 @@ params:
   default: 50
 render: |
   type: carwings
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/citroen.yaml b/templates/definition/vehicle/citroen.yaml
index 9d588d14..c2de1084 100644
--- a/templates/definition/vehicle/citroen.yaml
+++ b/templates/definition/vehicle/citroen.yaml
@@ -6,12 +6,4 @@ params:
   default: 50
 render: |
   type: citroen
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/ds.yaml b/templates/definition/vehicle/ds.yaml
index 5349df95..30896b7b 100644
--- a/templates/definition/vehicle/ds.yaml
+++ b/templates/definition/vehicle/ds.yaml
@@ -6,12 +6,4 @@ params:
   default: 50
 render: |
   type: ds
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml
index 2cc02376..cc2f986a 100644
--- a/templates/definition/vehicle/fiat.yaml
+++ b/templates/definition/vehicle/fiat.yaml
@@ -9,15 +9,7 @@ params:
   default: 42
 render: |
   type: fiat
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
   {{- if ne .pin "" }}
   pin: {{ .pin }} #mandatory to deep refresh SoC
   {{- end }}
diff --git a/templates/definition/vehicle/ford.yaml b/templates/definition/vehicle/ford.yaml
index 95e3164c..b4893f2a 100644
--- a/templates/definition/vehicle/ford.yaml
+++ b/templates/definition/vehicle/ford.yaml
@@ -8,12 +8,4 @@ params:
   default: 10
 render: |
   type: ford
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/hyundai.yaml b/templates/definition/vehicle/hyundai.yaml
index b6262dcd..418fc41a 100644
--- a/templates/definition/vehicle/hyundai.yaml
+++ b/templates/definition/vehicle/hyundai.yaml
@@ -6,9 +6,4 @@ params:
   default: 64
 render: |
   type: hyundai
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/kia.yaml b/templates/definition/vehicle/kia.yaml
index 907cb036..72d4d592 100644
--- a/templates/definition/vehicle/kia.yaml
+++ b/templates/definition/vehicle/kia.yaml
@@ -6,9 +6,4 @@ params:
   default: 64
 render: |
   type: kia
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml
index f499607f..51d69055 100644
--- a/templates/definition/vehicle/mini.yaml
+++ b/templates/definition/vehicle/mini.yaml
@@ -8,12 +8,4 @@ params:
   default: 32
 render: |
   type: mini
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/nissan.yaml b/templates/definition/vehicle/nissan.yaml
index 59dcc0d9..9d88d8c7 100644
--- a/templates/definition/vehicle/nissan.yaml
+++ b/templates/definition/vehicle/nissan.yaml
@@ -6,9 +6,4 @@ params:
   default: 60
 render: |
   type: nissan
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/opel.yaml b/templates/definition/vehicle/opel.yaml
index be56a138..8deb392b 100644
--- a/templates/definition/vehicle/opel.yaml
+++ b/templates/definition/vehicle/opel.yaml
@@ -8,12 +8,4 @@ params:
   default: 50
 render: |
   type: opel
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/peugeot.yaml b/templates/definition/vehicle/peugeot.yaml
index 30d360dc..06e3fd01 100644
--- a/templates/definition/vehicle/peugeot.yaml
+++ b/templates/definition/vehicle/peugeot.yaml
@@ -6,12 +6,4 @@ params:
   default: 50
 render: |
   type: peugeot
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/porsche.yaml b/templates/definition/vehicle/porsche.yaml
index 6663dd43..52b43957 100644
--- a/templates/definition/vehicle/porsche.yaml
+++ b/templates/definition/vehicle/porsche.yaml
@@ -1,20 +1,9 @@
 template: porsche
 description: Porsche
-loglevel: porsche
 paramsbase: vehicle
 params:
 - name: capacity
   default: 83.4
 render: |
   type: porsche
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if .capacity }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if .vin }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/renault.yaml b/templates/definition/vehicle/renault.yaml
index 84c65060..f76df27c 100644
--- a/templates/definition/vehicle/renault.yaml
+++ b/templates/definition/vehicle/renault.yaml
@@ -8,12 +8,4 @@ params:
   default: 60
 render: |
   type: renault
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  capacity: {{ .capacity }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/seat.yaml b/templates/definition/vehicle/seat.yaml
index 952a121c..aa62d826 100644
--- a/templates/definition/vehicle/seat.yaml
+++ b/templates/definition/vehicle/seat.yaml
@@ -6,14 +6,4 @@ params:
   default: 10
 render: |
   type: seat
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if ne .capacity "" }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/skoda.yaml b/templates/definition/vehicle/skoda.yaml
index 2ad70358..901db02e 100644
--- a/templates/definition/vehicle/skoda.yaml
+++ b/templates/definition/vehicle/skoda.yaml
@@ -6,14 +6,4 @@ params:
   default: 10
 render: |
   type: skoda
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if ne .capacity "" }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/skodaenyaq.yaml b/templates/definition/vehicle/skodaenyaq.yaml
index 00078c15..ed547bf3 100644
--- a/templates/definition/vehicle/skodaenyaq.yaml
+++ b/templates/definition/vehicle/skodaenyaq.yaml
@@ -6,14 +6,4 @@ params:
   default: 50
 render: |
   type: enyaq
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if ne .capacity "" }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/volvo.yaml b/templates/definition/vehicle/volvo.yaml
index ebdd2b24..b76bad99 100644
--- a/templates/definition/vehicle/volvo.yaml
+++ b/templates/definition/vehicle/volvo.yaml
@@ -6,14 +6,4 @@ params:
   default: 50
 render: |
   type: volvo
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if ne .capacity "" }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/vw.yaml b/templates/definition/vehicle/vw.yaml
index 0343bc74..d1009335 100644
--- a/templates/definition/vehicle/vw.yaml
+++ b/templates/definition/vehicle/vw.yaml
@@ -8,14 +8,4 @@ params:
   default: 10
 render: |
   type: vw
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if ne .capacity "" }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/templates/definition/vehicle/vwid.yaml b/templates/definition/vehicle/vwid.yaml
index ec421e5f..84ac0f35 100644
--- a/templates/definition/vehicle/vwid.yaml
+++ b/templates/definition/vehicle/vwid.yaml
@@ -1,6 +1,5 @@
 template: id
 description: Volkswagen (We Connect ID)
-loglevel: id
 paramsbase: vehicle
 params:
 - name: vin
@@ -9,14 +8,4 @@ params:
   default: 50
 render: |
   type: id
-  {{- if ne .title "" }}
-  title: {{ .title }}
-  {{- end }}
-  user: {{ .user }}
-  password: {{ .password }}
-  {{- if ne .capacity "" }}
-  capacity: {{ .capacity }}
-  {{- end }}
-  {{- if ne .vin "" }}
-  vin: {{ .vin }}
-  {{- end }}
+  {{include "vehicle-common" .}}
diff --git a/util/templates/proxy.tpl b/util/templates/proxy.tpl
index 7f4677dc..62ddc122 100644
--- a/util/templates/proxy.tpl
+++ b/util/templates/proxy.tpl
@@ -7,4 +7,9 @@ description: {{ .Description }}
 {{ .Name }}:
 	{{- if len .Value }} {{ .Value }} {{ end }}
 	{{- if .Help.DE }} # {{ .Help.DE }} {{- end }}
+{{- if ne (len .Values) 0 }} 
+{{- range .Values }}
+- {{ . }}
+{{- end }}
+{{- end }}
 {{ end -}}
diff --git a/util/templates/template.go b/util/templates/template.go
index 429c749e..b1b7673c 100644
--- a/util/templates/template.go
+++ b/util/templates/template.go
@@ -4,10 +4,13 @@ import (
 	"bytes"
 	_ "embed"
 	"fmt"
+	"strings"
 	"text/template"
 
 	"github.com/Masterminds/sprig/v3"
+	"github.com/evcc-io/evcc/templates/definition"
 	"github.com/evcc-io/evcc/util"
+	"gopkg.in/yaml.v3"
 )
 
 const (
@@ -45,10 +48,11 @@ const (
 var HemsValueTypes = []string{HemsTypeSMA}
 
 const (
-	ParamValueTypeString = "string"
-	ParamValueTypeNumber = "number"
-	ParamValueTypeFloat  = "float"
-	ParamValueTypeBool   = "bool"
+	ParamValueTypeString     = "string"
+	ParamValueTypeNumber     = "number"
+	ParamValueTypeFloat      = "float"
+	ParamValueTypeBool       = "bool"
+	ParamValueTypeStringList = "stringlist"
 )
 
 var ParamValueTypes = []string{ParamValueTypeString, ParamValueTypeNumber, ParamValueTypeBool}
@@ -111,6 +115,7 @@ type Param struct {
 	Help      TextLanguage // cli configuration help
 	Test      string       // testing default value
 	Value     string       // user provided value via cli configuration
+	Values    []string     // user provided list of values
 	ValueType string       // string representation of the value type, "string" is default
 	Choice    []string     // defines which usage choices this config supports, valid elemtents are "grid", "pv", "battery", "charge"
 	Usages    []string
@@ -118,11 +123,17 @@ type Param struct {
 	Comset    string // device specific default for modbus RS485 comset
 }
 
+type ParamBase struct {
+	Params []Param
+	Render string
+}
+
+var paramBaseList map[string]ParamBase
+
 // Template describes is a proxy device for use with cli and automated testing
 type Template struct {
 	Template     string
 	Description  string // user friendly description of the device this template describes
-	LogLevel     string // the implementation type of the device, equal to the type value under "Render"
 	Requirements Requirements
 	GuidedSetup  GuidedSetup
 	Generic      bool   // if this describes a generic device type rather than a product
@@ -131,31 +142,30 @@ type Template struct {
 	Render       string // rendering template
 }
 
-var paramBases = map[string][]Param{
-	"vehicle": {
-		{Name: "title"},
-		{Name: "user", Required: true},
-		{Name: "password", Required: true, Mask: true},
-		{Name: "vin", Example: "W..."},
-		{Name: "capacity", Default: "50", ValueType: ParamValueTypeFloat},
-	},
-}
-
 // add the referenced base Params and overwrite existing ones
 func (t *Template) ResolveParamBase() {
 	if t.ParamsBase == "" {
 		return
 	}
 
-	base, ok := paramBases[t.ParamsBase]
+	if paramBaseList == nil {
+		err := yaml.Unmarshal([]byte(definition.ParamBaseListDefinition), &paramBaseList)
+		if err != nil {
+			fmt.Printf("Error: failed to parse paramBasesDefinition: %v\n", err)
+			return
+		}
+	}
+
+	base, ok := paramBaseList[t.ParamsBase]
 	if !ok {
+		fmt.Printf("Error: Could not find parambase definition: %s\n", t.ParamsBase)
 		return
 	}
 
 	currentParams := make([]Param, len(t.Params))
 	copy(currentParams, t.Params)
-	t.Params = make([]Param, len(base))
-	copy(t.Params, base)
+	t.Params = make([]Param, len(base.Params))
+	copy(t.Params, base.Params)
 	for _, p := range currentParams {
 		if i, item := t.paramWithName(p.Name); item != nil {
 			// we only allow overwriting a few fields
@@ -175,12 +185,17 @@ func (t *Template) ResolveParamBase() {
 func (t *Template) Defaults(docsOrTests bool) map[string]interface{} {
 	values := make(map[string]interface{})
 	for _, p := range t.Params {
-		if p.Test != "" {
-			values[p.Name] = p.Test
-		} else if p.Example != "" && docsOrTests {
-			values[p.Name] = p.Example
-		} else {
-			values[p.Name] = p.Default // may be empty
+		switch p.ValueType {
+		case ParamValueTypeStringList:
+			values[p.Name] = []string{}
+		default:
+			if p.Test != "" {
+				values[p.Name] = p.Test
+			} else if p.Example != "" && docsOrTests {
+				values[p.Name] = p.Example
+			} else {
+				values[p.Name] = p.Default // may be empty
+			}
 		}
 	}
 
@@ -230,25 +245,39 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, includeD
 
 	for index, p := range t.Params {
 		for k, v := range values {
-			if p.Name == k {
-				t.Params[index].Value = v.(string)
+			if p.Name != k {
+				continue
+			}
+
+			switch p.ValueType {
+			case ParamValueTypeStringList:
+				for _, e := range v.([]string) {
+					t.Params[index].Values = append(p.Values, yamlQuote(e))
+				}
+			default:
+				t.Params[index].Value = yamlQuote(v.(string))
 			}
 		}
 	}
 
-	// remove params with no values, no defaults and no example
+	// remove params with no values
 	var newParams []Param
 	for _, param := range t.Params {
-		if param.Value == "" && param.Default == "" && param.Example == "" && !param.Required {
-			continue
+		if !param.Required {
+			switch param.ValueType {
+			case ParamValueTypeStringList:
+				if len(param.Values) == 0 {
+					continue
+				}
+			default:
+				if param.Value == "" {
+					continue
+				}
+			}
 		}
 		newParams = append(newParams, param)
 	}
 
-	for index, p := range newParams {
-		newParams[index].Value = yamlQuote(p.Value)
-	}
-
 	t.Params = newParams
 
 	out := new(bytes.Buffer)
@@ -273,8 +302,24 @@ func (t *Template) RenderResult(docs bool, other map[string]interface{}) ([]byte
 
 	t.ModbusValues(values)
 
+	// add the common templates
+	for _, v := range paramBaseList {
+		if !strings.Contains(t.Render, v.Render) {
+			t.Render = fmt.Sprintf("%s\n%s", t.Render, v.Render)
+		}
+	}
+
 	for item, p := range values {
-		values[item] = yamlQuote(fmt.Sprintf("%v", p))
+		switch p := p.(type) {
+		case []string:
+			var list []string
+			for _, v := range p {
+				list = append(list, yamlQuote(v))
+			}
+			values[item] = list
+		default:
+			values[item] = yamlQuote(fmt.Sprintf("%v", p))
+		}
 	}
 
 	tmpl := template.New("yaml")
diff --git a/vehicle/volvo.go b/vehicle/volvo.go
index 71078422..e58180d9 100644
--- a/vehicle/volvo.go
+++ b/vehicle/volvo.go
@@ -125,9 +125,9 @@ func (v *Volvo) Status() (api.ChargeStatus, error) {
 		switch res.HvBattery.HvBatteryChargeStatusDerived {
 		case "CableNotPluggedInCar":
 			return api.StatusA, nil
-		case "CablePluggedInCar", "CablePluggedInCar_FullyCharged":
+		case "CablePluggedInCar", "CablePluggedInCar_FullyCharged", "CablePluggedInCar_ChargingPaused":
 			return api.StatusB, nil
-		case "Charging":
+		case "Charging", "CablePluggedInCar_Charging":
 			return api.StatusC, nil
 		}
 	}


Test patch:
diff --git a/charger/config_test.go b/charger/config_test.go
index c36423ee..19fe8b42 100644
--- a/charger/config_test.go
+++ b/charger/config_test.go
@@ -23,6 +23,7 @@ var acceptable = []string{
 	"can only have either uri or device", // modbus
 	"sponsorship required, see https://github.com/evcc-io/evcc#sponsorship",
 	"eebus not configured",
+	"unexpected status: 400", // easee
 }
 
 func TestConfigChargers(t *testing.T) {
diff --git a/charger/evsewifi_test.go b/charger/evsewifi_test.go
index d0c016b9..784ad7ee 100644
--- a/charger/evsewifi_test.go
+++ b/charger/evsewifi_test.go
@@ -43,7 +43,7 @@ func TestEvseWifi(t *testing.T) {
 
 func TestEvseWifiEx(t *testing.T) {
 	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		_, _ = fmt.Fprintln(w, `{"list":[{"actualCurrentMA":600}]}`)
+		_, _ = fmt.Fprintln(w, `{"list":[{"actualCurrentMA":600, "alwaysActive":true}]}`)
 	}))
 	defer ts.Close()