Added Currents support to MCC and SMA (#156)

* Added Currents support to MCC

* Added currents to SMA
This commit is contained in:
Andreas Linde 2020-05-15 22:33:37 +02:00 • committed by GitHub
parent ca139564f3
commit 23f51bc56d
5 changed files with 180 additions and 39 deletions

View file

@ -42,7 +42,8 @@ type MCCCurrentSession struct {
// MCCEnergyPhase is the apiEnergy response for a single phase
type MCCEnergyPhase struct {
Power float64
Ampere float64
Power float64
}
// MCCEnergy is the apiEnergy response
@ -338,3 +339,11 @@ func (mcc *MobileConnect) ChargingTime() (time.Duration, error) {
return time.Duration(currentSession.Duration * time.Second), nil
}
// Currents implements the MeterCurrent interface
func (mcc *MobileConnect) Currents() (float64, float64, float64, error) {
var energy MCCEnergy
err := mcc.getEscapedJSON(mcc.apiURL(mccAPIEnergy), &energy)
return energy.L1.Ampere, energy.L2.Ampere, energy.L3.Ampere, err
}

View file

@ -67,7 +67,7 @@ func NewTestMobileConnect(t *testing.T, responses []apiResponse) *MobileConnect
return mcc
}
func TestMobileConnect_login(t *testing.T) {
func TestMobileConnectLogin(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -90,7 +90,7 @@ func TestMobileConnect_login(t *testing.T) {
}
}
func TestMobileConnect_refresh(t *testing.T) {
func TestMobileConnectRefresh(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -112,7 +112,7 @@ func TestMobileConnect_refresh(t *testing.T) {
}
}
func TestMobileConnect_Status(t *testing.T) {
func TestMobileConnectStatus(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -146,7 +146,7 @@ func TestMobileConnect_Status(t *testing.T) {
}
}
func TestMobileConnect_Enabled(t *testing.T) {
func TestMobileConnectEnabled(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -179,7 +179,7 @@ func TestMobileConnect_Enabled(t *testing.T) {
}
}
func TestMobileConnect_MaxCurrent(t *testing.T) {
func TestMobileConnectMaxCurrent(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -237,7 +237,7 @@ func TestMobileConnect_MaxCurrent(t *testing.T) {
}
}
func TestMobileConnect_CurrentPower(t *testing.T) {
func TestMobileConnectCurrentPower(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -252,6 +252,13 @@ func TestMobileConnect_CurrentPower(t *testing.T) {
},
2308, false,
},
{
"3 phase low power - charging",
[]apiResponse{
{mccAPIEnergy, "\"{\\n \\\"L1\\\": {\\n \\\"Ampere\\\": 0.5,\\n \\\"Power\\\": 7,\\n \\\"Volts\\\": 244.40000000000001\\n },\\n \\\"L2\\\": {\\n \\\"Ampere\\\": 0.5,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 242.10000000000002\\n },\\n \\\"L3\\\": {\\n \\\"Ampere\\\": 0.5,\\n \\\"Power\\\": 1,\\n \\\"Volts\\\": 242.30000000000001\\n }\\n}\\n\""},
},
8, false,
},
{
"no data response",
[]apiResponse{
@ -281,7 +288,7 @@ func TestMobileConnect_CurrentPower(t *testing.T) {
}
}
func TestMobileConnect_ChargedEnergy(t *testing.T) {
func TestMobileConnectChargedEnergy(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -324,7 +331,7 @@ func TestMobileConnect_ChargedEnergy(t *testing.T) {
}
}
func TestMobileConnect_ChargingTime(t *testing.T) {
func TestMobileConnectChargingTime(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
@ -365,3 +372,60 @@ func TestMobileConnect_ChargingTime(t *testing.T) {
})
}
}
func TestMobileConnectCurrents(t *testing.T) {
tests := []struct {
name string
responses []apiResponse
wantL1, wantL2, wantL3 float64
wantErr bool
}{
// test cases for software version 2914
{
"home plug - charging",
[]apiResponse{
{mccAPIEnergy, "\"{\\n \\\"L1\\\": {\\n \\\"Ampere\\\": 9.9000000000000004,\\n \\\"Power\\\": 2308,\\n \\\"Volts\\\": 230.5\\n },\\n \\\"L2\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 13.700000000000001\\n },\\n \\\"L3\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 13.9\\n }\\n}\\n\""},
},
9.9000000000000004, 0, 0, false,
},
{
"3 phase low power - charging",
[]apiResponse{
{mccAPIEnergy, "\"{\\n \\\"L1\\\": {\\n \\\"Ampere\\\": 0.5,\\n \\\"Power\\\": 7,\\n \\\"Volts\\\": 244.40000000000001\\n },\\n \\\"L2\\\": {\\n \\\"Ampere\\\": 0.5,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 242.10000000000002\\n },\\n \\\"L3\\\": {\\n \\\"Ampere\\\": 0.5,\\n \\\"Power\\\": 1,\\n \\\"Volts\\\": 242.30000000000001\\n }\\n}\\n\""},
},
0.5, 0.5, 0.5, false,
},
{
"no data response",
[]apiResponse{
{mccAPIEnergy, "\"\"\n"},
}, 0, 0, 0, false,
},
{
"home plug - error response",
[]apiResponse{
{mccAPIEnergy, "\"{\\n \\\"L1\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 246.60000000000002\\n },\\n \\\"L2\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 16.800000000000001\\n },\\n \\\"L3\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 16.300000000000001\\n }\\n}\\n\""},
}, 0, 0, 0, false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mcc := NewTestMobileConnect(t, tc.responses)
gotL1, gotL2, gotL3, err := mcc.Currents()
if (err != nil) != tc.wantErr {
t.Errorf("MobileConnect.Currents() error = %v, wantErr %v", err, tc.wantErr)
return
}
if gotL1 != tc.wantL1 {
t.Errorf("MobileConnect.Currents() = %v, want %v", gotL1, tc.wantL1)
}
if gotL2 != tc.wantL2 {
t.Errorf("MobileConnect.Currents() = %v, want %v", gotL2, tc.wantL2)
}
if gotL3 != tc.wantL3 {
t.Errorf("MobileConnect.Currents() = %v, want %v", gotL3, tc.wantL3)
}
})
}
}

View file

@ -17,17 +17,20 @@ const (
// SMA supporting SMA Home Manager 2.0 and SMA Energy Meter 30
type SMA struct {
log *util.Logger
uri string
serial string
power float64
energy float64
powerO sma.Obis
energyO sma.Obis
updated time.Time
recv chan sma.Telegram
mux sync.Mutex
once sync.Once
log *util.Logger
uri string
serial string
power float64
energy float64
currentL1 float64
currentL2 float64
currentL3 float64
powerO sma.Obis
energyO sma.Obis
updated time.Time
recv chan sma.Telegram
mux sync.Mutex
once sync.Once
}
// NewSMAFromConfig creates a SMA Meter from generic config
@ -94,7 +97,7 @@ func (sm *SMA) waitForInitialValue() {
}
// update the actual meter data
func (sm *SMA) updatePower(msg sma.Telegram) {
func (sm *SMA) updateMeterValues(msg sma.Telegram) {
sm.mux.Lock()
if sm.powerO != "" {
@ -117,6 +120,27 @@ func (sm *SMA) updatePower(msg sma.Telegram) {
}
}
if currentL1, ok := msg.Values[sma.CurrentL1]; ok {
sm.currentL1 = currentL1
sm.updated = time.Now()
} else {
sm.log.WARN.Println("missing obis for currentL1")
}
if currentL2, ok := msg.Values[sma.CurrentL2]; ok {
sm.currentL2 = currentL2
sm.updated = time.Now()
} else {
sm.log.WARN.Println("missing obis for currentL2")
}
if currentL3, ok := msg.Values[sma.CurrentL3]; ok {
sm.currentL3 = currentL3
sm.updated = time.Now()
} else {
sm.log.WARN.Println("missing obis for currentL3")
}
sm.mux.Unlock()
}
@ -127,7 +151,7 @@ func (sm *SMA) receive() {
continue
}
sm.updatePower(msg)
sm.updateMeterValues(msg)
}
}
@ -144,6 +168,19 @@ func (sm *SMA) CurrentPower() (float64, error) {
return sm.power, nil
}
// Currents implements the MeterCurrent interface
func (sm *SMA) Currents() (float64, float64, float64, error) {
sm.once.Do(sm.waitForInitialValue)
sm.mux.Lock()
defer sm.mux.Unlock()
if time.Since(sm.updated) > udpTimeout {
return 0, 0, 0, errors.New("recv timeout")
}
return sm.currentL1, sm.currentL2, sm.currentL3, nil
}
// SMAEnergy decorates SMA with api.MeterEnergy interface
type SMAEnergy struct {
*SMA

View file

@ -21,10 +21,13 @@ const (
msgPreamble = 28 // preamble size in bytes
msgCodeLength = 4 // length in bytes
ImportPower Obis = "1:1.4.0" // Wirkleistung (W)
ExportPower Obis = "1:2.4.0" // Wirkleistung (W)
ImportEnergy Obis = "1:1.8.0" // Wirkarbeit (Ws) +
ExportEnergy Obis = "1:2.8.0" // Wirkarbeit (Ws) −
ImportPower Obis = "1:1.4.0" // Wirkleistung (W)
ExportPower Obis = "1:2.4.0" // Wirkleistung (W)
ImportEnergy Obis = "1:1.8.0" // Wirkarbeit (Ws) +
ExportEnergy Obis = "1:2.8.0" // Wirkarbeit (Ws) −
CurrentL1 Obis = "1:31.4.0" // Strom (A)
CurrentL2 Obis = "1:51.4.0" // Strom (A)
CurrentL3 Obis = "1:71.4.0" // Strom (A)
)
// obisDefinition defines the properties needed to parse the SMA multicast telegram values
@ -50,7 +53,7 @@ var knownObisCodes = map[Obis]obisDefinition{
"1:24.4.0": {4, 0.1}, "1:24.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:29.4.0": {4, 0.1}, "1:29.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:30.4.0": {4, 0.1}, "1:30.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
"1:31.4.0": {4, 0.001}, // Strom (A)
CurrentL1: {4, 0.001}, // Strom (A)
"1:32.4.0": {4, 0.001}, // Spannung (V
// Phase 2: {
"1:41.4.0": {4, 0.1}, "1:41.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) +
@ -59,7 +62,7 @@ var knownObisCodes = map[Obis]obisDefinition{
"1:44.4.0": {4, 0.1}, "1:44.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:49.4.0": {4, 0.1}, "1:49.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:50.4.0": {4, 0.1}, "1:50.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
"1:51.4.0": {4, 0.001}, // Strom (A)
CurrentL2: {4, 0.001}, // Strom (A)
"1:52.4.0": {4, 0.001}, // Spannung (V)
// Phase 3: {
"1:61.4.0": {4, 0.1}, "1:61.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) +
@ -68,7 +71,7 @@ var knownObisCodes = map[Obis]obisDefinition{
"1:64.4.0": {4, 0.1}, "1:64.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:69.4.0": {4, 0.1}, "1:69.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:70.4.0": {4, 0.1}, "1:70.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
"1:71.4.0": {4, 0.001}, // Strom (A)
CurrentL3: {4, 0.001}, // Strom (A)
"1:72.4.0": {4, 0.001}, // Spannung (V)
// Others
"144:0.0.0": {4, 1}, // SW Version

View file

@ -7,31 +7,46 @@ import (
"github.com/andig/evcc/util"
)
func TestSMAUpdatePower(t *testing.T) {
func TestSMAUpdateMeterValues(t *testing.T) {
tests := []struct {
name string
messsage sma.Telegram
wantPower float64
name string
messsage sma.Telegram
wantPower float64
wantCurrentL1 float64
wantCurrentL2 float64
wantCurrentL3 float64
}{
{
"success export",
sma.Telegram{
Values: map[string]float64{
"1:1.4.0": 0,
"1:2.4.0": 37.9,
"1:1.4.0": 0,
"1:2.4.0": 37.9,
"1:31.4.0": 2.549,
"1:51.4.0": 0.397,
"1:71.4.0": 0.614,
},
},
-37.9,
2.549,
0.397,
0.614,
},
{
"success import",
sma.Telegram{
Values: map[string]float64{
"1:1.4.0": 20,
"1:2.4.0": 0,
"1:1.4.0": 20,
"1:2.4.0": 0,
"1:31.4.0": 0.654,
"1:51.4.0": 0.245,
"1:71.4.0": 0.231,
},
},
20,
0.654,
0.245,
0.231,
},
}
for _, tt := range tests {
@ -40,10 +55,23 @@ func TestSMAUpdatePower(t *testing.T) {
log: util.NewLogger("sma "),
}
sm.updatePower(tt.messsage)
sm.updateMeterValues(tt.messsage)
if sm.power != tt.wantPower {
t.Errorf("Listener.processMessage() got %v, want %v", sm.power, tt.wantPower)
t.Errorf("Listener.processMessage() got Power %v, want %v", sm.power, tt.wantPower)
}
if sm.currentL1 != tt.wantCurrentL1 {
t.Errorf("Listener.processMessage() got CurrentL1 %v, want %v", sm.currentL1, tt.wantCurrentL1)
}
if sm.currentL2 != tt.wantCurrentL2 {
t.Errorf("Listener.processMessage() got CurrentL2 %v, want %v", sm.currentL2, tt.wantCurrentL2)
}
if sm.currentL3 != tt.wantCurrentL3 {
t.Errorf("Listener.processMessage() got CurrentL3 %v, want %v", sm.currentL3, tt.wantCurrentL3)
}
})
}
}