Repository: evcc-io/evcc
Pull request #6835 (2023-03-14 23:33:59)

Homematic: Enhance Error Messages
Raise/Forward CCU fault codes and description

Refer to page 30 of https://homematic-ip.com/sites/default/files/downloads/HM_XmlRpc_API.pdf

CCU Error messages will be raised like this:
```bash
thierolm@LAPTOP-KQMMVN3H:~/git-repos/evcc$ ./evcc -c ./evcc_homematictest.yaml -l debug charger
[main  ] INFO 2023/03/15 13:25:47 evcc 0.114.1 (da3ae27d)
[main  ] INFO 2023/03/15 13:25:47 using config file: ./evcc_homematictest.yaml
[db    ] INFO 2023/03/15 13:25:47 using sqlite database: /home/thierolm/.evcc/evcc.db
Power:         Invalid device (-2)
Energy:        Invalid device (-2)
Charge status: Invalid device (-2)
Enabled:       Invalid device (-2)
Features:      []
``` 



Patch:
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index 9cee4218..3b0589f0 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -28,6 +28,9 @@ func (lp *Loadpoint) setPhases(phases int) {
 		lp.phases = phases
 		lp.Unlock()
 
+		// publish updated phase configuration
+		lp.publish(phasesEnabled, lp.phases)
+
 		// reset timer to disabled state
 		lp.resetPhaseTimer()
 
diff --git a/meter/homematic/connection.go b/meter/homematic/connection.go
index 31775e25..30f0f854 100644
--- a/meter/homematic/connection.go
+++ b/meter/homematic/connection.go
@@ -84,10 +84,6 @@ func (c *Connection) XmlCmd(method, channel string, values ...Param) (MethodResp
 		return hmr, err
 	}
 
-	if strings.Contains(string(res), "faultCode") {
-		return hmr, fmt.Errorf("ccu: %s", string(res))
-	}
-
 	// correct Homematic IP Legacy API (CCU port 2010) method response encoding value
 	res = []byte(strings.Replace(string(res), "ISO-8859-1", "UTF-8", 1))
 
@@ -98,7 +94,7 @@ func (c *Connection) XmlCmd(method, channel string, values ...Param) (MethodResp
 		return hmr, err
 	}
 
-	return hmr, err
+	return hmr, parseError(hmr)
 }
 
 // Initialze CCU methods via system.listMethods call
@@ -149,3 +145,26 @@ func (c *Connection) GridTotalEnergy() (float64, error) {
 	res, err := c.XmlCmd("getValue", c.MeterChannel, Param{CCUString: "IEC_ENERGY_COUNTER"})
 	return res.Value.CCUFloat, err
 }
+
+// parseError checks on Homematic CCU error codes
+// Refer to page 30 of https://homematic-ip.com/sites/default/files/downloads/HM_XmlRpc_API.pdf
+func parseError(res MethodResponse) error {
+	var faultCode int64
+	var faultString string
+
+	faultCode = 0
+	for _, f := range res.Fault {
+		if f.Name == "faultCode" {
+			faultCode = f.Value.CCUInt
+		}
+		if f.Name == "faultString" {
+			faultString = f.Value.CCUString
+		}
+	}
+
+	if faultCode != 0 {
+		return fmt.Errorf("%s (%v)", faultString, faultCode)
+	}
+
+	return nil
+}
diff --git a/meter/homematic/types.go b/meter/homematic/types.go
index f9ed85b5..ee0d84ca 100644
--- a/meter/homematic/types.go
+++ b/meter/homematic/types.go
@@ -21,7 +21,19 @@ type MethodCall struct {
 	Params     []Param  `xml:"params>param,omitempty"`
 }
 
+type Member struct {
+	Name  string     `xml:"name,omitempty"`
+	Value FaultValue `xml:"value,omitempty"`
+}
+
+type FaultValue struct {
+	XMLName   xml.Name `xml:"value"`
+	CCUString string   `xml:",chardata"`
+	CCUInt    int64    `xml:"i4,omitempty"`
+}
+
 type MethodResponse struct {
 	XMLName xml.Name `xml:"methodResponse"`
 	Value   Param    `xml:"params>param,omitempty"`
+	Fault   []Member `xml:"fault>value>struct>member,omitempty"`
 }
diff --git a/util/request/functions.go b/util/request/functions.go
index 84cc1f1a..b9d7d14d 100644
--- a/util/request/functions.go
+++ b/util/request/functions.go
@@ -36,20 +36,20 @@ func NewStatusError(resp *http.Response) StatusError {
 }
 
 func (e StatusError) Error() string {
-	return fmt.Sprintf("unexpected status: %d", e.resp.StatusCode)
+	return fmt.Sprintf("unexpected status: %d (%s)", e.resp.StatusCode, http.StatusText(e.resp.StatusCode))
 }
 
-// Response returns the respose with the unexpected error
+// Response returns the response with the unexpected error
 func (e StatusError) Response() *http.Response {
 	return e.resp
 }
 
-// StatusCode returns the respose's status code
+// StatusCode returns the response's status code
 func (e StatusError) StatusCode() int {
 	return e.resp.StatusCode
 }
 
-// HasStatus returns true if the respose's status code matches any of the given codes
+// HasStatus returns true if the response's status code matches any of the given codes
 func (e StatusError) HasStatus(codes ...int) bool {
 	for _, code := range codes {
 		if e.resp.StatusCode == code {


Test patch:
diff --git a/meter/homematic/types_test.go b/meter/homematic/types_test.go
new file mode 100644
index 00000000..9d999248
--- /dev/null
+++ b/meter/homematic/types_test.go
@@ -0,0 +1,37 @@
+package homematic
+
+import (
+	"encoding/xml"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// Test MethodResponse response
+func TestUnmarshalMethodResponse(t *testing.T) {
+
+	{
+		// Double response test
+		var res MethodResponse
+
+		xmlstr := `<?xml version="1.0" encoding="ISO-8859-1"?><methodResponse><params><param><value><double>20698.0</double></value></param></params></methodResponse>`
+		assert.NoError(t, xml.Unmarshal([]byte(strings.Replace(string(xmlstr), "ISO-8859-1", "UTF-8", 1)), &res))
+
+		assert.Equal(t, float64(20698), res.Value.CCUFloat)
+	}
+
+	{
+		// Faulty response test
+		var res MethodResponse
+
+		xmlstr := `<?xml version="1.0" encoding="ISO-8859-1"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><i4>-2</i4></value></member><member><name>faultString</name><value>Invalid device</value></member></struct></value></fault></methodResponse>`
+		assert.NoError(t, xml.Unmarshal([]byte(strings.Replace(string(xmlstr), "ISO-8859-1", "UTF-8", 1)), &res))
+
+		assert.Equal(t, "faultCode", res.Fault[0].Name)
+		assert.Equal(t, int64(-2), res.Fault[0].Value.CCUInt)
+		assert.Equal(t, "faultString", res.Fault[1].Name)
+		assert.Equal(t, "Invalid device", res.Fault[1].Value.CCUString)
+	}
+
+}