From 23f51bc56d5be21f5e4a2f9a0fb79bd870d781ba Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Fri, 15 May 2020 22:33:37 +0200 Subject: [PATCH] Added Currents support to MCC and SMA (#156) * Added Currents support to MCC * Added currents to SMA --- charger/mcc.go | 11 +++++- charger/mcc_test.go | 80 ++++++++++++++++++++++++++++++++++++++----- meter/sma.go | 63 +++++++++++++++++++++++++++------- meter/sma/listener.go | 17 +++++---- meter/sma_test.go | 48 ++++++++++++++++++++------ 5 files changed, 180 insertions(+), 39 deletions(-) diff --git a/charger/mcc.go b/charger/mcc.go index 3c7eb0788..944ca4a46 100644 --- a/charger/mcc.go +++ b/charger/mcc.go @@ -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 +} diff --git a/charger/mcc_test.go b/charger/mcc_test.go index d6c902283..f9e3be3f8 100644 --- a/charger/mcc_test.go +++ b/charger/mcc_test.go @@ -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) + } + }) + } +} diff --git a/meter/sma.go b/meter/sma.go index a3fd11a98..27cbf55e4 100644 --- a/meter/sma.go +++ b/meter/sma.go @@ -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 diff --git a/meter/sma/listener.go b/meter/sma/listener.go index 9297ca233..aeed317d2 100644 --- a/meter/sma/listener.go +++ b/meter/sma/listener.go @@ -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 diff --git a/meter/sma_test.go b/meter/sma_test.go index 363b591ac..c78432bb9 100644 --- a/meter/sma_test.go +++ b/meter/sma_test.go @@ -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) + } + }) } }