Repository: evcc-io/evcc
Pull request #6084 (2023-02-05 22:44:05)

Tasmota StatusSNS Energy Voltage & Frequency as Float
Fixes #6082  pv meter issue.

andig: Titel passt nicht zur Änderung- wirklich Frequenz je Kanal?

Patch:
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index 8b3048ae..29518939 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -56,6 +56,14 @@ type API interface {
 	GetPlannerUnit() string
 	// GetPlan creates a charging plan
 	GetPlan(targetTime time.Time, maxPower float64) (time.Duration, api.Rates, error)
+	// GetEnableThreshold gets the loadpoint enable threshold
+	GetEnableThreshold() float64
+	// SetEnableThreshold sets loadpoint enable threshold
+	SetEnableThreshold(threshold float64)
+	// GetDisableThreshold gets the loadpoint disable threshold
+	GetDisableThreshold() float64
+	// SetDisableThreshold sets loadpoint disable threshold
+	SetDisableThreshold(threshold float64)
 
 	// RemoteControl sets remote status demand
 	RemoteControl(string, RemoteDemand)
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index 7a7b9541..5eb44c78 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -220,6 +220,40 @@ func (lp *Loadpoint) setTargetTime(finishAt time.Time) {
 	}
 }
 
+// GetEnableThreshold gets the loadpoint enable threshold
+func (lp *Loadpoint) GetEnableThreshold() float64 {
+	lp.Lock()
+	defer lp.Unlock()
+	return lp.Enable.Threshold
+}
+
+// SetEnableThreshold sets loadpoint enable threshold
+func (lp *Loadpoint) SetEnableThreshold(threshold float64) {
+	lp.Lock()
+	defer lp.Unlock()
+
+	if lp.Enable.Threshold != threshold {
+		lp.Enable.Threshold = threshold
+	}
+}
+
+// GetDisableThreshold gets the loadpoint enable threshold
+func (lp *Loadpoint) GetDisableThreshold() float64 {
+	lp.Lock()
+	defer lp.Unlock()
+	return lp.Disable.Threshold
+}
+
+// SetDisableThreshold sets loadpoint disable threshold
+func (lp *Loadpoint) SetDisableThreshold(threshold float64) {
+	lp.Lock()
+	defer lp.Unlock()
+
+	if lp.Disable.Threshold != threshold {
+		lp.Disable.Threshold = threshold
+	}
+}
+
 // RemoteControl sets remote status demand
 func (lp *Loadpoint) RemoteControl(source string, demand loadpoint.RemoteDemand) {
 	lp.Lock()
diff --git a/meter/tasmota/types.go b/meter/tasmota/types.go
index 14abd30f..4435ee82 100644
--- a/meter/tasmota/types.go
+++ b/meter/tasmota/types.go
@@ -78,8 +78,8 @@ type StatusSNSResponse struct {
 			ApparentPower  Channels
 			ReactivePower  Channels
 			Factor         Channels
-			Frequency      int
-			Voltage        int
+			Frequency      Channels
+			Voltage        Channels
 			Current        Channels
 		}
 
diff --git a/server/http.go b/server/http.go
index 6a364a1e..685b8da5 100644
--- a/server/http.go
+++ b/server/http.go
@@ -117,27 +117,28 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
 		loadpoint := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id+1)).Subrouter()
 
 		routes := map[string]route{
-			"mode":          {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)},
-			"minsoc":        {[]string{"POST", "OPTIONS"}, "/minsoc/{value:[0-9]+}", intHandler(pass(lp.SetMinSoc), lp.GetMinSoc)},
-			"mincurrent":    {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(pass(lp.SetMinCurrent), lp.GetMinCurrent)},
-			"maxcurrent":    {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(pass(lp.SetMaxCurrent), lp.GetMaxCurrent)},
-			"phases":        {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)},
-			"targetenergy":  {[]string{"POST", "OPTIONS"}, "/target/energy/{value:[0-9.]+}", floatHandler(pass(lp.SetTargetEnergy), lp.GetTargetEnergy)},
-			"targetsoc":     {[]string{"POST", "OPTIONS"}, "/target/soc/{value:[0-9]+}", intHandler(pass(lp.SetTargetSoc), lp.GetTargetSoc)},
-			"targettime":    {[]string{"POST", "OPTIONS"}, "/target/time/{time:[0-9TZ:.-]+}", targetTimeHandler(lp)},
-			"targettime2":   {[]string{"DELETE", "OPTIONS"}, "/target/time", targetTimeRemoveHandler(lp)},
-			"plan":          {[]string{"GET"}, "/target/plan", planHandler(lp)},
-			"vehicle":       {[]string{"POST", "OPTIONS"}, "/vehicle/{vehicle:[1-9][0-9]*}", vehicleHandler(site, lp)},
-			"vehicle2":      {[]string{"DELETE", "OPTIONS"}, "/vehicle", vehicleRemoveHandler(lp)},
-			"vehicleDetect": {[]string{"PATCH", "OPTIONS"}, "/vehicle", vehicleDetectHandler(lp)},
-			"remotedemand":  {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source::[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
+			"mode":             {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)},
+			"minsoc":           {[]string{"POST", "OPTIONS"}, "/minsoc/{value:[0-9]+}", intHandler(pass(lp.SetMinSoc), lp.GetMinSoc)},
+			"mincurrent":       {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(pass(lp.SetMinCurrent), lp.GetMinCurrent)},
+			"maxcurrent":       {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(pass(lp.SetMaxCurrent), lp.GetMaxCurrent)},
+			"phases":           {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)},
+			"targetenergy":     {[]string{"POST", "OPTIONS"}, "/target/energy/{value:[0-9.]+}", floatHandler(pass(lp.SetTargetEnergy), lp.GetTargetEnergy)},
+			"targetsoc":        {[]string{"POST", "OPTIONS"}, "/target/soc/{value:[0-9]+}", intHandler(pass(lp.SetTargetSoc), lp.GetTargetSoc)},
+			"targettime":       {[]string{"POST", "OPTIONS"}, "/target/time/{time:[0-9TZ:.-]+}", targetTimeHandler(lp)},
+			"targettime2":      {[]string{"DELETE", "OPTIONS"}, "/target/time", targetTimeRemoveHandler(lp)},
+			"plan":             {[]string{"GET"}, "/target/plan", planHandler(lp)},
+			"vehicle":          {[]string{"POST", "OPTIONS"}, "/vehicle/{vehicle:[1-9][0-9]*}", vehicleHandler(site, lp)},
+			"vehicle2":         {[]string{"DELETE", "OPTIONS"}, "/vehicle", vehicleRemoveHandler(lp)},
+			"vehicleDetect":    {[]string{"PATCH", "OPTIONS"}, "/vehicle", vehicleDetectHandler(lp)},
+			"remotedemand":     {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source::[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
+			"enableThreshold":  {[]string{"POST", "OPTIONS"}, "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
+			"disableThreshold": {[]string{"POST", "OPTIONS"}, "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
 		}
 
 		for _, r := range routes {
 			loadpoint.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
 		}
 	}
-
 }
 
 // RegisterShutdownHandler connects the http handlers to the site
diff --git a/server/mqtt.go b/server/mqtt.go
index c29f67f9..64275696 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -151,6 +151,16 @@ func (m *MQTT) listenSetters(topic string, site site.API, lp loadpoint.API) {
 			}
 		}
 	})
+	m.Handler.ListenSetter(topic+"/enable/threshold/set", func(payload string) {
+		if threshold, err := strconv.ParseFloat(payload, 64); err == nil {
+			lp.SetEnableThreshold(threshold)
+		}
+	})
+	m.Handler.ListenSetter(topic+"/disable/threshold/set", func(payload string) {
+		if threshold, err := strconv.ParseFloat(payload, 64); err == nil {
+			lp.SetDisableThreshold(threshold)
+		}
+	})
 }
 
 // Run starts the MQTT publisher for the MQTT API


Test patch:
diff --git a/meter/tasmota/types_test.go b/meter/tasmota/types_test.go
new file mode 100644
index 00000000..d1eb3363
--- /dev/null
+++ b/meter/tasmota/types_test.go
@@ -0,0 +1,38 @@
+package tasmota
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+// Test StatusSNS response of all known Tasmota flavours
+func TestUnmarshalStatusSNSResponse(t *testing.T) {
+	var res StatusSNSResponse
+
+	// Test cases for #6082
+	jsonstr := `{"StatusSNS":{"Time":"2023-02-05T20:31:48","ENERGY":{"TotalStartTime":"2023-02-05T11:04:13","Total":1290.3960,"Yesterday":0.8540,"Today":0.1730,"Power":47.11,"ApparentPower":0.0,"ReactivePower":0.0,"Factor":0.00,"Voltage":0.00,"Current":0.000}}}`
+	if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
+		t.Error(err)
+	}
+	if power, err := res.StatusSNS.Energy.Power.Channel(1); err == nil && power != 47.11 {
+		t.Error("StatusSNS.Energy.Power.Channel(1) != 47.11")
+	}
+
+	// Test case for #5731
+	jsonstr = `{"StatusSNS":{"Time":"2023-01-09T18:57:39","Switch1":"ON","Switch2":"OFF","ANALOG":{"Temperature":49.6},"ENERGY":{"TotalStartTime":"2023-01-09T13:59:15","Total":0.077,"Yesterday":0.000,"Today":0.077,"Power":[8.15,0],"ApparentPower":[5,0],"ReactivePower":[4,0],"Factor":[0.00,0.00],"Frequency":50,"Voltage":237,"Current":[0.000,0.000]},"TempUnit":"C"}}`
+	if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
+		t.Error(err)
+	}
+	if power, err := res.StatusSNS.Energy.Power.Channel(1); err == nil && power != 8.15 {
+		t.Error("StatusSNS.Energy.Power.Channel(1) != 8.15")
+	}
+
+	// Test case for #3787
+	jsonstr = `{"StatusSNS":{"Time":"2022-07-07T13:01:11","HTU21":{"Temperature":25.2,"Humidity":45.5},"SML":{"Total_in":34507.4761,"Total_out":14737.1422,"Power_curr":-894,"Meter_number":"0901454d48000000000"},"Gas":{"Count":1.84},"TempUnit":"C"}}`
+	if err := json.Unmarshal([]byte(jsonstr), &res); err != nil {
+		t.Error(err)
+	}
+	if res.StatusSNS.SML.PowerCurr != -894 {
+		t.Error("res.StatusSNS.SML.PowerCurr != -894")
+	}
+}