Repository: evcc-io/evcc
Pull request #2004 (2021-12-09 11:27:31)

Fix config templates


Patch:
diff --git a/charger/easee.go b/charger/easee.go
index 855b0d57..d8f433bd 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -20,6 +20,7 @@ package charger
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -40,6 +41,7 @@ import (
 type Easee struct {
 	*request.Helper
 	charger               string
+	site, circuit         int
 	updated               time.Time
 	chargeStatus          api.ChargeStatus
 	log                   *util.Logger
@@ -112,6 +114,27 @@ func NewEasee(user, password, charger string, circuit int, cache time.Duration)
 		}
 
 		c.charger = chargers[0].ID
+
+		// find site
+		site, err := c.chargerSite(c.charger)
+		if err != nil {
+			return nil, err
+		}
+
+		// find single charger per circuit
+		for _, circuit := range site.Circuits {
+			if len(circuit.Chargers) > 1 {
+				continue
+			}
+
+			for _, charger := range circuit.Chargers {
+				if charger.ID == c.charger {
+					c.site = site.ID
+					c.circuit = circuit.ID
+					break
+				}
+			}
+		}
 	}
 
 	client, err := signalr.NewClient(context.Background(),
@@ -145,6 +168,13 @@ func NewEasee(user, password, charger string, circuit int, cache time.Duration)
 	return c, err
 }
 
+func (c *Easee) chargerSite(charger string) (easee.Site, error) {
+	var res easee.Site
+	uri := fmt.Sprintf("%s/chargers/%s/site", easee.API, charger)
+	err := c.GetJSON(uri, &res)
+	return res, err
+}
+
 // connect creates an HTTP connection to the signalR hub
 func (c *Easee) connect(ts oauth2.TokenSource) func() (signalr.Connection, error) {
 	return func() (signalr.Connection, error) {
@@ -288,14 +318,10 @@ func (c *Easee) CommandResponse(i json.RawMessage) {
 	// c.observe("CommandResponse", i)
 }
 
-func (c *Easee) chargers() (res []easee.Charger, err error) {
+func (c *Easee) chargers() ([]easee.Charger, error) {
+	var res []easee.Charger
 	uri := fmt.Sprintf("%s/chargers", easee.API)
-
-	req, err := request.New(http.MethodGet, uri, nil, request.JSONEncoding)
-	if err == nil {
-		err = c.DoJSON(req, &res)
-	}
-
+	err := c.GetJSON(uri, &res)
 	return res, err
 }
 
@@ -368,18 +394,56 @@ var _ api.ChargePhases = (*Easee)(nil)
 
 // Phases1p3p implements the api.ChargePhases interface
 func (c *Easee) Phases1p3p(phases int) error {
-	if phases == 3 {
-		phases = 2
-	}
+	var err error
+	if c.circuit != 0 {
+		// circuit level
+		uri := fmt.Sprintf("%s/sites/%d/circuits/%d/settings", easee.API, c.site, c.circuit)
 
-	data := easee.ChargerSettings{
-		PhaseMode: &phases,
-	}
+		var res easee.CircuitSettings
+		if err := c.GetJSON(uri, &res); err != nil {
+			return err
+		}
 
-	uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
-	resp, err := c.Post(uri, request.JSONContent, request.MarshalJSON(data))
-	if err == nil {
-		resp.Body.Close()
+		if res.MaxCircuitCurrentP1 == nil || res.MaxCircuitCurrentP2 == nil || res.MaxCircuitCurrentP3 == nil {
+			return errors.New("MaxCircuitCurrent must not be nil")
+		}
+
+		var zero float64
+		max1 := float64(*res.MaxCircuitCurrentP1)
+		max2 := float64(*res.MaxCircuitCurrentP2)
+		max3 := float64(*res.MaxCircuitCurrentP3)
+
+		data := easee.CircuitSettings{
+			DynamicCircuitCurrentP1: &max1,
+			DynamicCircuitCurrentP2: &zero,
+			DynamicCircuitCurrentP3: &zero,
+		}
+
+		if phases > 1 {
+			data.DynamicCircuitCurrentP2 = &max2
+			data.DynamicCircuitCurrentP3 = &max3
+		}
+
+		var resp *http.Response
+		if resp, err = c.Post(uri, request.JSONContent, request.MarshalJSON(data)); err == nil {
+			resp.Body.Close()
+		}
+	} else {
+		// charger level
+		if phases == 3 {
+			phases = 2 // mode 2 means 3p
+		}
+
+		data := easee.ChargerSettings{
+			PhaseMode: &phases,
+		}
+
+		uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
+
+		var resp *http.Response
+		if resp, err = c.Post(uri, request.JSONContent, request.MarshalJSON(data)); err == nil {
+			resp.Body.Close()
+		}
 	}
 
 	return err
diff --git a/charger/easee/types.go b/charger/easee/types.go
index eae18d49..ed766da5 100644
--- a/charger/easee/types.go
+++ b/charger/easee/types.go
@@ -69,6 +69,7 @@ type Circuit struct {
 	RatedCurrent     float64
 	UseDynamicMaster bool
 	ParentCircuitID  int
+	Chargers         []Charger
 }
 
 // ChargerStatus is the charger status type
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/charger/wallbe.yaml b/templates/definition/charger/wallbe.yaml
index a9d331a9..45d5b002 100644
--- a/templates/definition/charger/wallbe.yaml
+++ b/templates/definition/charger/wallbe.yaml
@@ -10,7 +10,7 @@ params:
   required: true
   example: 192.0.2.2
 - name: port
-  default: 1883
+  default: 502
 render: |
   type: wallbe
   uri: {{ .host }}:{{ .port }}
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/templates/docs/charger/wallbe.yaml b/templates/docs/charger/wallbe.yaml
index 0c9c6acd..543e62f0 100644
--- a/templates/docs/charger/wallbe.yaml
+++ b/templates/docs/charger/wallbe.yaml
@@ -2,4 +2,4 @@ type: template
 template: wallbe
 description: Wallbe (Eco, Pro)
 host: 192.0.2.2 
-port: 1883
\ No newline at end of file
+port: 502
\ 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