Added Currents support to MCC and SMA (#156)
* Added Currents support to MCC * Added currents to SMA
This commit is contained in:
parent
ca139564f3
commit
23f51bc56d
5 changed files with 180 additions and 39 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
meter/sma.go
63
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue