Repository: evcc-io/evcc
Pull request #11933 (2024-01-29 09:10:30)

Sunspec: refactor model selection
Replace #11929

@premultiply ich bin mit den Templates noch nicht zu frieden:

- Es wird Model 160 verwendet- anscheinend gibts das aber nicht immer. Lieber 101/103?
- Für Battery wird fix 124 verwendet- dafür hätten wir ein separates Template. Dort gibts aber keine Leistung. Wie lösen wir das? Wie spielt 802 da rein?


premultiply: Bei Hybrid kann man ja 101/103 aus bekannten Gründen nicht nutzen. Daher ist es dabei die übliche Variante die PV- und Batteriedaten über Strings unter Model 160 abzubilden.
124 liefert dann noch den SoC. 802 ist da eher selten aber auch möglich.

Bei "normalen WR" kann man direkt den AC-Ausgang des Basismodells + SoC von 124|802 verwenden.
andig: > Bei "normalen WR" kann man direkt den AC-Ausgang des Basismodells + SoC von 124|802 verwenden.

Wie würde man in dem Fall die Batterieleistung bekommen falls der WR aus PV und Batterie gespeist würde? Oder gibts den Fall nicht weil das dann eben genau ein "hybrider" wäre?
premultiply: Genau, das muss dann ein Hybrid sein.
"Normale" WR haben immer nur eine Funktion. PV oder Batterie.

Müsste `124|802:ChaState` eigentlich funktionieren?
andig: > Müsste 124|802:ChaState eigentlich funktionieren?

Leider nein- bei der 124 ist das `% AhrRtg`, bei der 820 gibts das nicht- da heisst es `SoC` (`%WHRtg`). Wo auch immer der semantische Unterschied ist...
premultiply: Mmmmmh.... `124:ChaState|802:SoC` 🤔
andig: Langsam wirds ein bissl viel Magie :O. Aber ja, liesse sich bestimmt machen. Dann sollten wir die Syntax aber komplett umstellen und/oder einfach aus `value` ein Slice machen, dann brauchts die Magie nicht :)
andig: Mal als Experiment- müsste bei den anderen Templates nachgezogen werden.
premultiply:  It's magic... hex, hex :)

Gefällt mir so richtig gut!
andig: Juuut- nur was machen wir mit der Leistung für die Batterie?
premultiply: Tja...
Nicht so einfach.

Oft liegt die ja auf 160:3:DCW und 160:4:DCW bei Hybrid. Aber eine richtige Spezifikation gibt es dafür nicht.

Bei Normal ist es einfach: Die AC-Leistung des Basismodells.
andig: > Bei "normalen WR" kann man direkt den AC-Ausgang des Basismodells + SoC von 124|802 verwenden.

Done. 
andig: @premultiply rein damit?

Patch:
diff --git a/cmd/flags.go b/cmd/flags.go
index 5e019e33..0436d4be 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -40,6 +40,7 @@ const (
 
 	flagDigits = "digits"
 	flagDelay  = "delay"
+	flagForce  = "force"
 )
 
 func bind(cmd *cobra.Command, key string, flagName ...string) {
diff --git a/cmd/settings-get.go b/cmd/settings-get.go
index 325db4a0..12f4ecc8 100644
--- a/cmd/settings-get.go
+++ b/cmd/settings-get.go
@@ -3,9 +3,7 @@ package cmd
 import (
 	"fmt"
 	"os"
-	"os/signal"
 	"regexp"
-	"syscall"
 	"text/tabwriter"
 
 	"github.com/evcc-io/evcc/server/db/settings"
@@ -49,14 +47,4 @@ func runSettingsGet(cmd *cobra.Command, args []string) {
 		fmt.Fprintf(w, "%s:\t%s\n", s.Key, s.Value)
 	}
 	w.Flush()
-
-	// catch signals
-	go func() {
-		signalC := make(chan os.Signal, 1)
-		signal.Notify(signalC, os.Interrupt, syscall.SIGTERM)
-
-		<-signalC // wait for signal
-
-		os.Exit(1)
-	}()
 }
diff --git a/cmd/settings-set.go b/cmd/settings-set.go
new file mode 100644
index 00000000..cfdbabe2
--- /dev/null
+++ b/cmd/settings-set.go
@@ -0,0 +1,57 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/AlecAivazis/survey/v2"
+	"github.com/evcc-io/evcc/server/db/settings"
+	"github.com/spf13/cobra"
+)
+
+// settingsSetCmd represents the configure command
+var settingsSetCmd = &cobra.Command{
+	Use:   "set",
+	Short: "Set configuration setting",
+	Run:   runSettingsSet,
+	Args:  cobra.ExactArgs(2),
+}
+
+func init() {
+	settingsCmd.AddCommand(settingsSetCmd)
+	settingsSetCmd.Flags().BoolP(flagForce, "f", false, "Force (no confirmation)")
+}
+
+func runSettingsSet(cmd *cobra.Command, args []string) {
+	// load config
+	if err := loadConfigFile(&conf); err != nil {
+		log.FATAL.Fatal(err)
+	}
+
+	// setup environment
+	if err := configureEnvironment(cmd, conf); err != nil {
+		log.FATAL.Fatal(err)
+	}
+
+	confirmation, _ := cmd.Flags().GetBool(flagForce)
+	if !confirmation {
+		msg := fmt.Sprintf("Set %s", args[0])
+		if val, _ := settings.String(args[0]); val != "" {
+			msg = fmt.Sprintf("Override %s (current value: %s)", args[0], val)
+		}
+
+		prompt := &survey.Confirm{
+			Message: msg,
+		}
+
+		if err := survey.AskOne(prompt, &confirmation); err != nil {
+			log.FATAL.Fatal(err)
+		}
+	}
+
+	if confirmation {
+		settings.SetString(args[0], args[1])
+	}
+
+	// wait for shutdown
+	<-shutdownDoneC()
+}
diff --git a/provider/go.go b/provider/go.go
index ecf13871..d9e4610a 100644
--- a/provider/go.go
+++ b/provider/go.go
@@ -159,7 +159,7 @@ func (p *Go) handleSetter(param string, val any) error {
 func (p *Go) evaluate() (res any, err error) {
 	defer func() {
 		if r := recover(); r != nil {
-			err = fmt.Errorf("go script panic: %v", r)
+			err = fmt.Errorf("panic: %v", r)
 		}
 	}()
 
diff --git a/provider/modbus_sunspec.go b/provider/modbus_sunspec.go
index aa9a5d29..39d7aec1 100644
--- a/provider/modbus_sunspec.go
+++ b/provider/modbus_sunspec.go
@@ -31,7 +31,7 @@ func init() {
 func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error) {
 	cc := struct {
 		modbus.Settings `mapstructure:",squash"`
-		Value           string
+		Value           []string
 		Scale           float64
 		Delay           time.Duration
 		ConnectDelay    time.Duration
@@ -67,7 +67,7 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error)
 	log := util.NewLogger("sunspec")
 	conn.Logger(log.TRACE)
 
-	if cc.Value == "" {
+	if len(cc.Value) == 0 {
 		return nil, errors.New("value is required")
 	}
 
@@ -77,9 +77,13 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error)
 		return nil, err
 	}
 
-	ops, err := modbus.ParsePoint(cc.Value)
-	if err != nil {
-		return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
+	var ops []modbus.SunSpecOperation
+	for _, val := range cc.Value {
+		op, err := modbus.ParsePoint(val)
+		if err != nil {
+			return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
+		}
+		ops = append(ops, op)
 	}
 
 	mb := &ModbusSunspec{
diff --git a/templates/definition/meter/sunspec-hybrid.yaml b/templates/definition/meter/sunspec-hybrid.yaml
index 72c0fa53..4c44d1e0 100644
--- a/templates/definition/meter/sunspec-hybrid.yaml
+++ b/templates/definition/meter/sunspec-hybrid.yaml
@@ -15,59 +15,104 @@ params:
     advanced: true
 render: |
   {{- if eq .usage "grid" }}
-  type: mbmd
-  {{- include "modbus" . }}
+  type: custom
   # sunspec model 203 (int+sf)/ 213 (float) meter
-  power: 203|213:W
-  energy: 203|213:TotWhImp
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:W
+      - 213:W
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:TotWhImp
+      - 213:TotWhImp
   currents:
-    - 203|213:AphA
-    - 203|213:AphB
-    - 203|213:AphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphA
+        - 213:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphB
+        - 213:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphC
+        - 213:AphC
   voltages:
-    - 203|213:PhVphA
-    - 203|213:PhVphB
-    - 203|213:PhVphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphA
+        - 213:PhVphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphB
+        - 213:PhVphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphC
+        - 213:PhVphC
   powers:
-    - 203|213:WphA
-    - 203|213:WphB
-    - 203|213:WphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphA
+        - 213:WphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphB
+        - 213:WphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphC
+        - 213:WphC
   {{- end }}
   {{- if eq .usage "pv" }}
   type: custom
   power:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:1:DCW # mppt 1
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:2:DCW # mppt 2
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:1:DCW # mppt 1
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:2:DCW # mppt 2
   energy:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:1:DCWH # mppt 1
-      scale: 0.001
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:2:DCWH # mppt 2
-      scale: 0.001
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:1:DCWH # mppt 1
+        scale: 0.001
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:2:DCWH # mppt 2
+        scale: 0.001
   {{- end }}
   {{- if eq .usage "battery" }}
   type: custom
   power:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:3:DCW # mppt 3 (charge)
-      scale: -1
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:4:DCW # mppt 4 (discharge)
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:3:DCW # mppt 3 (charge)
+        scale: -1
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:4:DCW # mppt 4 (discharge)
   energy:
     source: sunspec
     {{- include "modbus" . | indent 2 }}
@@ -76,8 +121,8 @@ render: |
   soc:
     source: sunspec
     {{- include "modbus" . | indent 2 }}
-    value: 124:ChaState
-  {{- if .capacity }}
+    value:
+      - 124:ChaState
+      - 802:SoC
   capacity: {{ .capacity }} # kWh
   {{- end }}
-  {{- end }}
diff --git a/templates/definition/meter/sunspec-inverter.yaml b/templates/definition/meter/sunspec-inverter.yaml
index 031227db..8a7ac6d1 100644
--- a/templates/definition/meter/sunspec-inverter.yaml
+++ b/templates/definition/meter/sunspec-inverter.yaml
@@ -9,63 +9,96 @@ params:
     choice: ["grid", "pv", "battery"]
   - name: modbus
     choice: ["tcpip", "rs485"]
-  - name: integer
-    description:
-      de: Integer Registerzugriff (nur für Netzzähler)
-      en: Integer register access (grid meter only)
-    help:
-      de: Einstellung Float/Integer im Wechselrichter überprüfen
-      en: Verify Float/Integer setting in inverter
-    advanced: true
-    type: bool
   - name: capacity
     advanced: true
 render: |
-  type: mbmd
-  {{- include "modbus" . }}
   {{- if eq .usage "grid" }}
-  model: sunspec
-  {{- if eq .integer "true" }}
-  # sunspec model 203 (int+sf) meter
-  power: 203:W
-  energy: 203:TotWhImp
-  currents:
-    - 203:AphA
-    - 203:AphB
-    - 203:AphC
-  voltages:
-    - 203:PhVphA
-    - 203:PhVphB
-    - 203:PhVphC
-  powers:
-    - 203:WphA
-    - 203:WphB
-    - 203:WphC
-  {{ else }}
-  # sunspec model 213 (float) meter
-  power: 213:W
-  energy: 213:TotWhImp
+  type: custom
+  # sunspec model 203 (int+sf)/ 213 (float) meter
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:W
+      - 213:W
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:TotWhImp
+      - 213:TotWhImp
   currents:
-    - 213:AphA
-    - 213:AphB
-    - 213:AphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphA
+        - 213:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphB
+        - 213:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphC
+        - 213:AphC
   voltages:
-    - 213:PhVphA
-    - 213:PhVphB
-    - 213:PhVphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphA
+        - 213:PhVphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphB
+        - 213:PhVphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphC
+        - 213:PhVphC
   powers:
-    - 213:WphA
-    - 213:WphB
-    - 213:WphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphA
+        - 213:WphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphB
+        - 213:WphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphC
+        - 213:WphC
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  type: custom
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value:
+      - 101:W
+      - 103:W
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value:
+      - 101:WH
+      - 103:WH
+    scale: 0.001
   {{- end }}
-  {{- else }}
-  model: sunspec
-  power: Power
-  energy: Export
   {{- if eq .usage "battery" }}
-  soc: 124:ChaState
-  {{- if .capacity }}
+  type: custom
+  soc:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value:
+      - 124:ChaState
+      - 802:SoC
   capacity: {{ .capacity }} # kWh
   {{- end }}
-  {{- end }}  
-  {{- end }}
diff --git a/util/modbus/mbmd.go b/util/modbus/mbmd.go
index 7ceeece8..ec9693eb 100644
--- a/util/modbus/mbmd.go
+++ b/util/modbus/mbmd.go
@@ -21,11 +21,9 @@ func ParseOperation(dev meters.Device, measurement string) (Operation, error) {
 	// if measurement cannot be parsed it could be SunSpec model/block/point
 	op.MBMD.IEC61850, err = meters.MeasurementString(measurement)
 	if err != nil {
-		suns, err := ParsePoint(measurement)
-		if err == nil {
-			op.SunSpec = suns[0]
+		if op.SunSpec, err = ParsePoint(measurement); err != nil {
+			return op, err
 		}
-		return op, err
 	}
 
 	// for RS485 check if producer supports the measurement
diff --git a/util/modbus/sunspec.go b/util/modbus/sunspec.go
index 363ef73c..8b862910 100644
--- a/util/modbus/sunspec.go
+++ b/util/modbus/sunspec.go
@@ -13,39 +13,30 @@ type SunSpecOperation struct {
 }
 
 // ParsePoint parses sunspec point from string
-func ParsePoint(selector string) ([]SunSpecOperation, error) {
+func ParsePoint(selector string) (SunSpecOperation, error) {
+	var (
+		res SunSpecOperation
+		err error
+	)
+
 	el := strings.Split(selector, ":")
 	if len(el) < 2 || len(el) > 3 {
-		return nil, fmt.Errorf("invalid sunspec format: %s", selector)
+		return res, fmt.Errorf("invalid sunspec format: %s", selector)
 	}
 
-	models := strings.Split(el[0], "|")
-	if len(models) == 0 {
-		return nil, fmt.Errorf("missing sunspec model: %s", selector)
+	if res.Model, err = strconv.Atoi(el[0]); err != nil {
+		return res, fmt.Errorf("invalid sunspec model: %s", selector)
 	}
 
-	var res []SunSpecOperation
-	for _, m := range models {
-		model, err := strconv.Atoi(m)
+	if len(el) == 3 {
+		// block is the middle element
+		res.Block, err = strconv.Atoi(el[1])
 		if err != nil {
-			return nil, fmt.Errorf("invalid sunspec model: %s", selector)
-		}
-
-		var block int
-		if len(el) == 3 {
-			// block is the middle element
-			block, err = strconv.Atoi(el[1])
-			if err != nil {
-				return nil, fmt.Errorf("invalid sunspec block: %s", selector)
-			}
+			return res, fmt.Errorf("invalid sunspec block: %s", selector)
 		}
-
-		res = append(res, SunSpecOperation{
-			Model: model,
-			Block: block,
-			Point: el[len(el)-1],
-		})
 	}
 
+	res.Point = el[len(el)-1]
+
 	return res, nil
 }


Test patch:
diff --git a/util/modbus/modbus_test.go b/util/modbus/modbus_test.go
index bf87c71e..70a18082 100644
--- a/util/modbus/modbus_test.go
+++ b/util/modbus/modbus_test.go
@@ -9,11 +9,10 @@ import (
 func TestParsePoint(t *testing.T) {
 	tc := []struct {
 		in  string
-		ops []SunSpecOperation
+		ops SunSpecOperation
 	}{
-		{"103:W", []SunSpecOperation{{103, 0, "W"}}},
-		{"802:1:V", []SunSpecOperation{{802, 1, "V"}}},
-		{"101|103:DCW", []SunSpecOperation{{101, 0, "DCW"}, {103, 0, "DCW"}}},
+		{"103:W", SunSpecOperation{103, 0, "W"}},
+		{"802:1:V", SunSpecOperation{802, 1, "V"}},
 	}
 
 	for _, tc := range tc {