Repository: evcc-io/evcc
Pull request #2005 (2021-12-09 19:49:11)

More configure flow improvements
- Added Tinkerforge Warp help texts to make it clear that the MQTT host and port need to be added
- Allow to define linked templates in guided setup to be added multiple times, e.g. multiple SMA PV or Battery inverters with an SMA Home Manager setup
- Allow to exclude a linked template, if devices are added of another linked template, e.g. don't ask for SMA PV inverters if an SMA Energy Meter is added
- Various other changes


Patch:
diff --git a/cmd/configure/flow.go b/cmd/configure/flow.go
index 0737d4f8..30084667 100644
--- a/cmd/configure/flow.go
+++ b/cmd/configure/flow.go
@@ -96,51 +96,89 @@ func (c *CmdConfigure) configureDeviceGuidedSetup() {
 func (c *CmdConfigure) configureLinkedTypes(templateItem templates.Template) {
 	linkedTemplates := templateItem.GuidedSetup.Linked
 
+	deviceOfTemplateAdded := make(map[string]bool)
+
 	if linkedTemplates == nil {
 		return
 	}
 
 	for _, linkedTemplate := range linkedTemplates {
-		for ok := true; ok; {
-			deviceItem := device{}
-
-			linkedTemplateItem, err := templates.ByTemplate(linkedTemplate.Template, string(DeviceClassMeter))
-			if err != nil {
-				fmt.Println("Error: " + err.Error())
-				return
+		if linkedTemplate.ExcludeTemplate != "" {
+			// don't process this linked template if a referenced exclude template was added
+			if deviceOfTemplateAdded[linkedTemplate.ExcludeTemplate] {
+				continue
 			}
-			if len(linkedTemplateItem.Params) == 0 || linkedTemplate.Usage == "" {
-				return
+		}
+
+		linkedTemplateItem, err := templates.ByTemplate(linkedTemplate.Template, string(DeviceClassMeter))
+		if err != nil {
+			fmt.Println("Error: " + err.Error())
+			return
+		}
+		if len(linkedTemplateItem.Params) == 0 || linkedTemplate.Usage == "" {
+			break
+		}
+
+		category := DeviceCategory(linkedTemplate.Usage)
+
+		localizeMap := localizeMap{
+			"Linked":     linkedTemplateItem.Description,
+			"Article":    DeviceCategories[category].article,
+			"Additional": DeviceCategories[category].additional,
+			"Category":   DeviceCategories[category].title,
+		}
+
+		fmt.Println()
+		if !c.askYesNo(c.localizedString("AddLinkedDeviceInCategory", localizeMap)) {
+			continue
+		}
+
+		for ok := true; ok; {
+			if added := c.configureLinkedTemplate(linkedTemplateItem, category); added {
+				deviceOfTemplateAdded[linkedTemplate.Template] = true
 			}
 
-			category := DeviceCategory(linkedTemplate.Usage)
+			if !linkedTemplate.Multiple {
+				break
+			}
 
 			fmt.Println()
-			if !c.askYesNo(c.localizedString("AddLinkedDeviceInCategory", localizeMap{"Linked": linkedTemplateItem.Description, "Article": DeviceCategories[category].article, "Category": DeviceCategories[category].title})) {
+			if !c.askYesNo(c.localizedString("AddAnotherLinkedDeviceInCategory", localizeMap)) {
 				break
 			}
+		}
+	}
+	fmt.Println("DONE")
+}
 
-			values := c.processConfig(linkedTemplateItem.Params, category)
-			deviceItem, err = c.processDeviceValues(values, linkedTemplateItem, deviceItem, category)
-			if err != nil {
-				if !errors.Is(err, c.errDeviceNotValid) {
-					fmt.Println()
-					fmt.Println(err)
-				}
-				fmt.Println()
-				if c.askConfigFailureNextStep() {
-					continue
-				}
-
-			} else {
-				c.configuration.AddDevice(deviceItem, category)
+// configureLinkedTemplate lets the user configure a device that is marked as being linked to a guided device
+// returns true if a device was added
+func (c *CmdConfigure) configureLinkedTemplate(template templates.Template, category DeviceCategory) bool {
+	for ok := true; ok; {
+		deviceItem := device{}
 
+		values := c.processConfig(template.Params, category)
+		deviceItem, err := c.processDeviceValues(values, template, deviceItem, category)
+		if err != nil {
+			if !errors.Is(err, c.errDeviceNotValid) {
 				fmt.Println()
-				fmt.Println(linkedTemplateItem.Description + " " + c.localizedString("Device_Added", nil))
+				fmt.Println(err)
 			}
-			break
+			fmt.Println()
+			if c.askConfigFailureNextStep() {
+				continue
+			}
+
+		} else {
+			c.configuration.AddDevice(deviceItem, category)
+
+			fmt.Println()
+			fmt.Println(template.Description + " " + c.localizedString("Device_Added", nil))
+			return true
 		}
+		break
 	}
+	return false
 }
 
 // configureDeviceCategory lets the user select and configure a device from a specific category
diff --git a/cmd/configure/localization/de.toml b/cmd/configure/localization/de.toml
index 306c258a..4dceba78 100644
--- a/cmd/configure/localization/de.toml
+++ b/cmd/configure/localization/de.toml
@@ -15,7 +15,8 @@ ItemNotPresent = "Mein Gerät ist nicht in der Liste"
 
 AddDeviceInCategory = "Möchtest du {{ .Article }} {{ .Category }} hinzufügen?"
 AddAnotherDeviceInCategory = "Möchtest du noch {{ .Additional }} {{ .Category }} hinzufügen?"
-AddLinkedDeviceInCategory = "Möchtest du '{{ .Linked }}' als {{ .Article }} {{ .Category }} hinzufügen?"
+AddLinkedDeviceInCategory = "Möchtest du ein '{{ .Linked }}' Gerät als {{ .Article }} {{ .Category }} hinzufügen?"
+AddAnotherLinkedDeviceInCategory = "Möchtest du noch ein '{{ .Linked }}' Gerät als {{ .Article }} {{ .Category }} hinzufügen?"
 
 Error = "Fehler: {{ .Error }}"
 Error_ItemNotPresent = "Gerät nicht vorhanden"
diff --git a/cmd/configure/localization/en.toml b/cmd/configure/localization/en.toml
index 65bcdc2b..3807a5a8 100644
--- a/cmd/configure/localization/en.toml
+++ b/cmd/configure/localization/en.toml
@@ -15,7 +15,8 @@ ItemNotPresent = "My device is not in this list"
 
 AddDeviceInCategory = "Do you want to add {{ .Article }} {{ .Category }}?"
 AddAnotherDeviceInCategory = "Do you want to add {{ .Additional }} {{ .Category }}?"
-AddLinkedDeviceInCategory = "Do you want to add '{{ .Linked }}' as {{ .Article }} {{ .Category }}?"
+AddLinkedDeviceInCategory = "Do you want to add a '{{ .Linked }}' device as {{ .Article }} {{ .Category }}?"
+AddAnotherLinkedDeviceInCategory = "Do you want to add another '{{ .Linked }}' device as {{ .Article }} {{ .Category }}?"
 
 Error = "Error: {{ .Error }}"
 Error_ItemNotPresent = "Device not present"
diff --git a/templates/README.md b/templates/README.md
index 161135ab..162b75d8 100644
--- a/templates/README.md
+++ b/templates/README.md
@@ -50,6 +50,16 @@ Allows to define a list of meter devices that are typically installed with this
 - `pv`:  for pv inverter/meter
 - `battery`: for battery inverter/meter
 
+#### `multiple`
+
+`multiple:true` to define that multiple devices of this template can be added
+
+#### `excludetemplate`
+
+`excludetemplate` defines a linked device `template` value. If defined and a device of the linked template is added, then this linked template won't be considered in the flow
+
+Example Use Case: With SMA Home Manager, there can be a SMA Energy Meter used for getting the PV generation or multiple SMA PV inverters. But never both together. So if the used added an SMA Energy Meter, then the flow shoudn't ask for SMA PV inverters.
+
 ## `requirements`
 
 `requirements` provides an option to define various requirements / dependencies that need to be setup
diff --git a/templates/definition/charger/nrgkick-bluetooth.yaml b/templates/definition/charger/nrgkick-bluetooth.yaml
index 9f5db683..17e2b1f9 100644
--- a/templates/definition/charger/nrgkick-bluetooth.yaml
+++ b/templates/definition/charger/nrgkick-bluetooth.yaml
@@ -5,6 +5,7 @@ params:
   required: true
 - name: pin
   required: true
+  mask: true
 render: |
   type: nrgkick-bluetooth
   mac: {{ .mac }}
diff --git a/templates/definition/charger/tinkerforge-warp-pro.yaml b/templates/definition/charger/tinkerforge-warp-pro.yaml
index 4844343d..bf1973d0 100644
--- a/templates/definition/charger/tinkerforge-warp-pro.yaml
+++ b/templates/definition/charger/tinkerforge-warp-pro.yaml
@@ -9,8 +9,14 @@ params:
 - name: host
   required: true
   example: 192.0.2.2
+  help:
+    de: Die IP Adresse oder der Hostname des MQTT Brokers
+    en: The IP address or hostname of the MQTT broker
 - name: port
   default: 1883
+  help:
+    de: Der Port des MQTT Brokers
+    en: The port of the MQTT broker
 - name: topic
   default: warp
 - name: timeout
diff --git a/templates/definition/charger/tinkerforge-warp.yaml b/templates/definition/charger/tinkerforge-warp.yaml
index af111caf..d8476ca2 100644
--- a/templates/definition/charger/tinkerforge-warp.yaml
+++ b/templates/definition/charger/tinkerforge-warp.yaml
@@ -9,8 +9,14 @@ params:
 - name: host
   required: true
   example: 192.0.2.2
+  help:
+    de: Die IP Adresse oder der Hostname des MQTT Brokers
+    en: The IP address or hostname of the MQTT broker
 - name: port
   default: 1883
+  help:
+    de: Der Port des MQTT Brokers
+    en: The port of the MQTT broker
 - name: topic
   default: warp
 - name: timeout
diff --git a/templates/definition/meter/powerfox-poweropti.yaml b/templates/definition/meter/powerfox-poweropti.yaml
index 5f03aba7..13c07991 100644
--- a/templates/definition/meter/powerfox-poweropti.yaml
+++ b/templates/definition/meter/powerfox-poweropti.yaml
@@ -7,6 +7,7 @@ params:
   required: true
 - name: password
   required: true
+  mask: true
 render: |
   type: custom
   power:
diff --git a/templates/definition/meter/sma-homemanager.yaml b/templates/definition/meter/sma-homemanager.yaml
index a3559627..e185e365 100644
--- a/templates/definition/meter/sma-homemanager.yaml
+++ b/templates/definition/meter/sma-homemanager.yaml
@@ -9,8 +9,11 @@ guidedsetup:
     usage: pv
   - template: sma-inverter
     usage: pv
+    multiple: true
+    excludetemplate: sma-energy-meter
   - template: sma-inverter
     usage: battery
+    multiple: true
 params:
 - name: usage
   choice: ["grid"]
diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml
index cc2f986a..57ff4019 100644
--- a/templates/definition/vehicle/fiat.yaml
+++ b/templates/definition/vehicle/fiat.yaml
@@ -3,6 +3,7 @@ description: Fiat
 paramsbase: vehicle
 params:
 - name: pin
+  mask: true
 - name: vin
   example: ZFAE...
 - name: capacity
diff --git a/templates/docs/charger/tinkerforge-warp-pro.yaml b/templates/docs/charger/tinkerforge-warp-pro.yaml
index 26a1881d..41e1e8d9 100644
--- a/templates/docs/charger/tinkerforge-warp-pro.yaml
+++ b/templates/docs/charger/tinkerforge-warp-pro.yaml
@@ -1,7 +1,7 @@
 type: template
 template: tinkerforge-warp-pro
 description: TinkerForge WARP Charger Pro
-host: 192.0.2.2 
-port: 1883 
+host: 192.0.2.2  # Die IP Adresse oder der Hostname des MQTT Brokers
+port: 1883  # Der Port des MQTT Brokers
 topic: warp 
 timeout: 30s
\ No newline at end of file
diff --git a/templates/docs/charger/tinkerforge-warp.yaml b/templates/docs/charger/tinkerforge-warp.yaml
index 1e120a1a..db4b2b40 100644
--- a/templates/docs/charger/tinkerforge-warp.yaml
+++ b/templates/docs/charger/tinkerforge-warp.yaml
@@ -1,7 +1,7 @@
 type: template
 template: tinkerforge-warp
 description: TinkerForge WARP Charger
-host: 192.0.2.2 
-port: 1883 
+host: 192.0.2.2  # Die IP Adresse oder der Hostname des MQTT Brokers
+port: 1883  # Der Port des MQTT Brokers
 topic: warp 
 timeout: 30s
\ No newline at end of file
diff --git a/util/templates/template.go b/util/templates/template.go
index b1b7673c..8848ab0b 100644
--- a/util/templates/template.go
+++ b/util/templates/template.go
@@ -100,8 +100,10 @@ type GuidedSetup struct {
 
 // Linked Template
 type LinkedTemplate struct {
-	Template string
-	Usage    string // usage: "grid", "pv", "battery"
+	Template        string
+	Usage           string // usage: "grid", "pv", "battery"
+	Multiple        bool   // if true, multiple instances of this template can be added
+	ExcludeTemplate string // only consider this if no device of the named linked template was added
 }
 
 // Param is a proxy template parameter


Test patch:
diff --git a/cmd/configure/devicetest.go b/cmd/configure/devicetest.go
index 0662385a..5ffeebbe 100644
--- a/cmd/configure/devicetest.go
+++ b/cmd/configure/devicetest.go
@@ -84,7 +84,7 @@ func (d *DeviceTest) testCharger(v interface{}) (DeviceTestResult, error) {
 			return DeviceTestResultInvalid, err
 		}
 	} else {
-		return DeviceTestResultInvalid, errors.New("Selected device is not a wallbox!")
+		return DeviceTestResultInvalid, errors.New("selected device is not a wallbox!")
 	}
 
 	if v, ok := v.(api.Meter); ok {
@@ -116,11 +116,11 @@ func (d *DeviceTest) testMeter(deviceCategory DeviceCategory, v interface{}) (De
 					return DeviceTestResultInvalid, err
 				}
 			} else {
-				return DeviceTestResultInvalid, errors.New("Selected device is not a battery meter!")
+				return DeviceTestResultInvalid, errors.New("selected device is not a battery meter!")
 			}
 		}
 	} else {
-		return DeviceTestResultInvalid, errors.New("Selected device is not a meter!")
+		return DeviceTestResultInvalid, errors.New("selected device is not a meter!")
 	}
 
 	return DeviceTestResultValid, nil
@@ -135,7 +135,7 @@ func (d *DeviceTest) testVehicle(v interface{}) (DeviceTestResult, error) {
 			}
 		}
 	} else {
-		return DeviceTestResultInvalid, errors.New("Selected device is not a vehicle!")
+		return DeviceTestResultInvalid, errors.New("selected device is not a vehicle!")
 	}
 
 	return DeviceTestResultValid, nil