Add data types and test coverage

This commit is contained in:
Rob Watson 2022-07-13 19:01:07 +02:00
parent e467102cfe
commit a103e2c4d8
7 changed files with 413 additions and 141 deletions

14
.drone.yml Normal file
View File

@ -0,0 +1,14 @@
---
kind: pipeline
type: kubernetes
name: default
steps:
- name: backend
image: golang:1.18
commands:
- go install honnef.co/go/tools/cmd/staticcheck@latest
- go build ./...
- go vet ./...
- staticcheck ./...
- go test -race -cover ./...

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# goodwe-go
A small project to read data from a Goodwe solar inverter using Go. Based on https://pypi.org/project/goodwe/.

11
go.mod
View File

@ -1,3 +1,14 @@
module git.netflux.io/rob/goodwe-go module git.netflux.io/rob/goodwe-go
go 1.18 go 1.18
require (
github.com/stretchr/testify v1.8.0
golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

17
go.sum Normal file
View File

@ -0,0 +1,17 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983 h1:sUweFwmLOje8KNfXAVqGGAsmgJ/F8jJ6wBLJDt4BTKY=
golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -8,15 +8,23 @@ import (
"io" "io"
"math" "math"
"strings" "strings"
"time"
"git.netflux.io/rob/goodwe-go/command" "git.netflux.io/rob/goodwe-go/command"
) )
// The timezone used to parse timestamps.
const locationName = "Europe/Madrid"
type ET struct { type ET struct {
SerialNumber string SerialNumber string
ModelName string ModelName string
} }
func (inv ET) isSinglePhase() bool {
return strings.Contains(inv.SerialNumber, "EHU")
}
// Unexported struct used for parsing binary data only. // Unexported struct used for parsing binary data only.
type etDeviceInfo struct { type etDeviceInfo struct {
ModbusVersion uint16 ModbusVersion uint16
@ -53,8 +61,20 @@ func (info *etDeviceInfo) toDeviceInfo() *DeviceInfo {
} }
// Unexported struct used for parsing binary data only. // Unexported struct used for parsing binary data only.
//
// Raw types are based partly on the the PyPI library, and partly on the
// third-party online documentation:
//
// https://github.com/marcelblijleven/goodwe/blob/327c7803e8415baeb4b6252431db91e1fc6f2fb3
// https://github.com/tkubec/GoodWe/wiki/ET-Series-Registers
//
// It's especially unclear whether fields should be parsed signed or unsigned.
// Handling differs in the above two sources. In most cases, overflowing a
// uint16 max value is unlikely but it may have an impact on handling negative
// values. To allow for the latter case, signed types are mostly preferred
// below.
type etRuntimeData struct { type etRuntimeData struct {
_ [6]byte Timestamp [6]byte
PV1Voltage int16 PV1Voltage int16
PV1Current int16 PV1Current int16
PV1Power int32 PV1Power int32
@ -117,8 +137,8 @@ type etRuntimeData struct {
WorkMode int32 WorkMode int32
OperationCode int16 OperationCode int16
ErrorCodes int16 ErrorCodes int16
PVGenerationTotal int32 EnergyGenerationTotal int32
PVGenerationToday int32 EnergyGenerationToday int32
EnergyExportTotal int32 EnergyExportTotal int32
EnergyExportTotalHours int32 EnergyExportTotalHours int32
EnergyExportToday int16 EnergyExportToday int16
@ -134,94 +154,118 @@ type etRuntimeData struct {
DiagStatusCode int32 DiagStatusCode int32
} }
func filterSinglePhase[T numeric](v T, singlePhase bool) T {
if singlePhase {
return 0
}
return v
}
// toRuntimeData panics if the `locationName` constant cannot be resolved to a
// time.Location.
func (data *etRuntimeData) toRuntimeData(singlePhase bool) *ETRuntimeData { func (data *etRuntimeData) toRuntimeData(singlePhase bool) *ETRuntimeData {
filterSinglePhase := func(i int) int { yr := data.Timestamp[0]
if singlePhase { mon := data.Timestamp[1]
return 0 day := data.Timestamp[2]
} hr := data.Timestamp[3]
return i min := data.Timestamp[4]
sec := data.Timestamp[5]
loc, err := time.LoadLocation(locationName)
if err != nil {
panic(fmt.Sprintf("unknown location: %s", locationName))
} }
return &ETRuntimeData{ return &ETRuntimeData{
PV1Voltage: int(data.PV1Voltage), Timestamp: time.Date(2000+int(yr), time.Month(mon), int(day), int(hr), int(min), int(sec), 0, loc),
PV1Current: int(data.PV1Current), PV1Voltage: newVoltage(data.PV1Voltage),
PV1Power: int(data.PV1Power), PV1Current: newCurrent(data.PV1Current),
PV2Voltage: int(data.PV2Voltage), PV1Power: newPower(data.PV1Power),
PV2Current: int(data.PV2Current), PV2Voltage: newVoltage(data.PV2Voltage),
PV2Power: int(data.PV2Power), PV2Current: newCurrent(data.PV2Current),
PVPower: int(data.PV1Power) + int(data.PV2Power), PV2Power: newPower(data.PV2Power),
PVPower: newPower(data.PV1Power + data.PV2Power),
PV2Mode: data.PV2Mode, PV2Mode: data.PV2Mode,
PV1Mode: data.PV1Mode, PV1Mode: data.PV1Mode,
OnGridL1Voltage: int(data.OnGridL1Voltage), OnGridL1Voltage: newVoltage(data.OnGridL1Voltage),
OnGridL1Current: int(data.OnGridL1Current), OnGridL1Current: newCurrent(data.OnGridL1Current),
OnGridL1Frequency: int(data.OnGridL1Frequency), OnGridL1Frequency: newFrequency(data.OnGridL1Frequency),
OnGridL1Power: int(data.OnGridL1Power), OnGridL1Power: newPower(data.OnGridL1Power),
OnGridL2Voltage: filterSinglePhase(int(data.OnGridL2Voltage)), OnGridL2Voltage: newVoltage(filterSinglePhase(data.OnGridL2Voltage, singlePhase)),
OnGridL2Current: filterSinglePhase(int(data.OnGridL2Current)), OnGridL2Current: newCurrent(filterSinglePhase(data.OnGridL2Current, singlePhase)),
OnGridL2Frequency: filterSinglePhase(int(data.OnGridL2Frequency)), OnGridL2Frequency: newFrequency(filterSinglePhase(data.OnGridL2Frequency, singlePhase)),
OnGridL2Power: filterSinglePhase(int(data.OnGridL2Power)), OnGridL2Power: newPower(filterSinglePhase(data.OnGridL2Power, singlePhase)),
OnGridL3Voltage: filterSinglePhase(int(data.OnGridL3Voltage)), OnGridL3Voltage: newVoltage(filterSinglePhase(data.OnGridL3Voltage, singlePhase)),
OnGridL3Current: filterSinglePhase(int(data.OnGridL3Current)), OnGridL3Current: newCurrent(filterSinglePhase(data.OnGridL3Current, singlePhase)),
OnGridL3Frequency: filterSinglePhase(int(data.OnGridL3Frequency)), OnGridL3Frequency: newFrequency(filterSinglePhase(data.OnGridL3Frequency, singlePhase)),
OnGridL3Power: filterSinglePhase(int(data.OnGridL3Power)), OnGridL3Power: newPower(filterSinglePhase(data.OnGridL3Power, singlePhase)),
GridMode: int(data.GridMode), GridMode: int(data.GridMode),
TotalInverterPower: int(data.TotalInverterPower), TotalInverterPower: newPower(data.TotalInverterPower),
ActivePower: int(data.ActivePower), ActivePower: newPower(data.ActivePower),
ReactivePower: int(data.ReactivePower), ReactivePower: int(data.ReactivePower),
ApparentPower: int(data.ApparentPower), ApparentPower: int(data.ApparentPower),
BackupL1Voltage: int(data.BackupL1Voltage), BackupL1Voltage: newVoltage(data.BackupL1Voltage),
BackupL1Current: int(data.BackupL1Current), BackupL1Current: newCurrent(data.BackupL1Current),
BackupL1Frequency: int(data.BackupL1Frequency), BackupL1Frequency: newFrequency(data.BackupL1Frequency),
LoadModeL1: int(data.LoadModeL1), LoadModeL1: int(data.LoadModeL1),
BackupL1Power: int(data.BackupL1Power), BackupL1Power: newPower(data.BackupL1Power),
BackupL2Voltage: filterSinglePhase(int(data.BackupL2Voltage)), BackupL2Voltage: newVoltage(filterSinglePhase(data.BackupL2Voltage, singlePhase)),
BackupL2Current: filterSinglePhase(int(data.BackupL2Current)), BackupL2Current: newCurrent(filterSinglePhase(data.BackupL2Current, singlePhase)),
BackupL2Frequency: filterSinglePhase(int(data.BackupL2Frequency)), BackupL2Frequency: newFrequency(filterSinglePhase(data.BackupL2Frequency, singlePhase)),
LoadModeL2: filterSinglePhase(int(data.LoadModeL2)), LoadModeL2: int(filterSinglePhase(data.LoadModeL2, singlePhase)),
BackupL2Power: filterSinglePhase(int(data.BackupL2Power)), BackupL2Power: newPower(filterSinglePhase(data.BackupL2Power, singlePhase)),
BackupL3Voltage: filterSinglePhase(int(data.BackupL3Voltage)), BackupL3Voltage: newVoltage(filterSinglePhase(data.BackupL3Voltage, singlePhase)),
BackupL3Current: filterSinglePhase(int(data.BackupL3Current)), BackupL3Current: newCurrent(filterSinglePhase(data.BackupL3Current, singlePhase)),
BackupL3Frequency: filterSinglePhase(int(data.BackupL3Frequency)), BackupL3Frequency: newFrequency(filterSinglePhase(data.BackupL3Frequency, singlePhase)),
LoadModeL3: filterSinglePhase(int(data.LoadModeL3)), LoadModeL3: int(filterSinglePhase(data.LoadModeL3, singlePhase)),
BackupL3Power: filterSinglePhase(int(data.BackupL3Power)), BackupL3Power: newPower(filterSinglePhase(data.BackupL3Power, singlePhase)),
LoadL1: int(data.LoadL1), LoadL1: newPower(data.LoadL1),
LoadL2: filterSinglePhase(int(data.LoadL2)), LoadL2: newPower(filterSinglePhase(data.LoadL2, singlePhase)),
LoadL3: filterSinglePhase(int(data.LoadL3)), LoadL3: newPower(filterSinglePhase(data.LoadL3, singlePhase)),
BackupLoad: int(data.BackupLoad), BackupLoad: newPower(data.BackupLoad),
Load: int(data.Load), Load: newPower(data.Load),
UPSLoad: int(data.UPSLoad), UPSLoad: int(data.UPSLoad),
TemperatureAir: int(data.TemperatureAir), TemperatureAir: newTemp(data.TemperatureAir),
TemperatureModule: int(data.TemperatureModule), TemperatureModule: newTemp(data.TemperatureModule),
Temperature: int(data.Temperature), Temperature: newTemp(data.Temperature),
FunctionBit: int(data.FunctionBit), FunctionBit: int(data.FunctionBit),
BusVoltage: int(data.BusVoltage), BusVoltage: newVoltage(data.BusVoltage),
NBusVoltage: int(data.NBusVoltage), NBusVoltage: newVoltage(data.NBusVoltage),
BatteryVoltage: int(data.BatteryVoltage), BatteryVoltage: newVoltage(data.BatteryVoltage),
BatteryCurrent: int(data.BatteryCurrent), BatteryCurrent: newCurrent(data.BatteryCurrent),
BatteryMode: int(data.BatteryMode), BatteryMode: int(data.BatteryMode),
WarningCode: int(data.WarningCode), WarningCode: int(data.WarningCode),
SafetyCountryCode: int(data.SafetyCountryCode), SafetyCountryCode: int(data.SafetyCountryCode),
WorkMode: int(data.WorkMode), WorkMode: int(data.WorkMode),
OperationCode: int(data.OperationCode), OperationCode: int(data.OperationCode),
ErrorCodes: int(data.ErrorCodes), ErrorCodes: int(data.ErrorCodes),
PVGenerationTotal: int(data.PVGenerationTotal), EnergyGenerationTotal: newEnergy(data.EnergyGenerationTotal),
PVGenerationToday: int(data.PVGenerationToday), EnergyGenerationToday: newEnergy(data.EnergyGenerationToday),
EnergyExportTotal: int(data.EnergyExportTotal), EnergyExportTotal: newEnergy(data.EnergyExportTotal),
EnergyExportTotalHours: int(data.EnergyExportTotalHours), EnergyExportTotalHours: int(data.EnergyExportTotalHours),
EnergyExportToday: int(data.EnergyExportToday), EnergyExportToday: newEnergy(data.EnergyExportToday),
EnergyImportTotal: int(data.EnergyImportTotal), EnergyImportTotal: newEnergy(data.EnergyImportTotal),
EnergyImportToday: int(data.EnergyImportToday), EnergyImportToday: newEnergy(data.EnergyImportToday),
EnergyLoadTotal: int(data.EnergyLoadTotal), EnergyLoadTotal: newEnergy(data.EnergyLoadTotal),
EnergyLoadDay: int(data.EnergyLoadDay), EnergyLoadDay: newEnergy(data.EnergyLoadDay),
BatteryChargeTotal: int(data.BatteryChargeTotal), BatteryChargeTotal: int(data.BatteryChargeTotal),
BatteryChargeToday: int(data.BatteryChargeToday), BatteryChargeToday: int(data.BatteryChargeToday),
BatteryDischargeTotal: int(data.BatteryDischargeTotal), BatteryDischargeTotal: int(data.BatteryDischargeTotal),
BatteryDischargeToday: int(data.BatteryDischargeToday), BatteryDischargeToday: int(data.BatteryDischargeToday),
DiagStatusCode: int(data.DiagStatusCode), DiagStatusCode: int(data.DiagStatusCode),
HouseConsumption: int32(float64(data.PV1Power) + float64(data.PV2Power) + math.Round(float64(data.BatteryVoltage)*float64(data.BatteryCurrent)) - float64(data.ActivePower)), HouseConsumption: Power(int32(float64(data.PV1Power) + float64(data.PV2Power) + math.Round(float64(data.BatteryVoltage)*float64(data.BatteryCurrent)) - float64(data.ActivePower))),
} }
} }
func (inv ET) DecodeRuntimeData(p []byte) (*ETRuntimeData, error) {
var runtimeData etRuntimeData
if err := binary.Read(bytes.NewReader(p), binary.BigEndian, &runtimeData); err != nil {
return nil, fmt.Errorf("error parsing response: %s", err)
}
return runtimeData.toRuntimeData(inv.isSinglePhase()), nil
}
// DEPRECATED
func (inv ET) DeviceInfo(ctx context.Context, conn io.ReadWriter) (*DeviceInfo, error) { func (inv ET) DeviceInfo(ctx context.Context, conn io.ReadWriter) (*DeviceInfo, error) {
resp, err := command.Send(command.NewModbus(command.ModbusCommandTypeRead, 0x88b8, 0x0021), conn) resp, err := command.Send(command.NewModbus(command.ModbusCommandTypeRead, 0x88b8, 0x0021), conn)
if err != nil { if err != nil {
@ -236,6 +280,7 @@ func (inv ET) DeviceInfo(ctx context.Context, conn io.ReadWriter) (*DeviceInfo,
return deviceInfo.toDeviceInfo(), nil return deviceInfo.toDeviceInfo(), nil
} }
// DEPRECATED
func (inv ET) RuntimeData(ctx context.Context, conn io.ReadWriter) (*ETRuntimeData, error) { func (inv ET) RuntimeData(ctx context.Context, conn io.ReadWriter) (*ETRuntimeData, error) {
deviceInfo, err := inv.DeviceInfo(ctx, conn) deviceInfo, err := inv.DeviceInfo(ctx, conn)
if err != nil { if err != nil {

141
inverter/et_test.go Normal file
View File

@ -0,0 +1,141 @@
package inverter_test
import (
"testing"
"time"
"git.netflux.io/rob/goodwe-go/inverter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDecodeDeviceInfo(t *testing.T) {
inBytes := []byte{22, 7, 13, 10, 35, 1, 12, 92, 0, 32, 0, 0, 3, 244, 8, 224, 0, 83, 0, 0, 7, 89, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 2, 2, 9, 63, 0, 123, 19, 135, 0, 0, 11, 129, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 0, 1, 0, 0, 11, 129, 0, 0, 3, 237, 127, 255, 255, 255, 255, 255, 255, 255, 9, 62, 0, 5, 19, 135, 0, 1, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 7, 148, 128, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 148, 0, 2, 2, 119, 127, 255, 1, 148, 1, 0, 14, 118, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 0, 1, 255, 255, 0, 0, 0, 0, 0, 0, 30, 19, 0, 0, 0, 44, 0, 0, 30, 19, 0, 0, 1, 15, 0, 46, 0, 0, 0, 0, 0, 0, 0, 0, 11, 195, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 2, 8, 64, 71, 0, 3, 0, 0, 255, 255}
t.Run("with multi-phase inverter", func(t *testing.T) {
inv := inverter.ET{SerialNumber: "foo"}
runtimeData, err := inv.DecodeRuntimeData(inBytes)
require.NoError(t, err)
wantLoc, _ := time.LoadLocation("Europe/Madrid")
assert.Equal(t, time.Date(2022, 7, 13, 10, 35, 1, 0, wantLoc), runtimeData.Timestamp)
assert.Equal(t, inverter.Voltage(316.4), runtimeData.PV1Voltage)
assert.Equal(t, inverter.Current(3.2), runtimeData.PV1Current)
assert.Equal(t, inverter.Power(1012), runtimeData.PV1Power)
assert.Equal(t, inverter.Voltage(227.2), runtimeData.PV2Voltage)
assert.Equal(t, inverter.Current(8.3), runtimeData.PV2Current)
assert.Equal(t, inverter.Power(1881), runtimeData.PV2Power)
assert.Equal(t, inverter.Power(2893), runtimeData.PVPower)
assert.Equal(t, inverter.Voltage(236.7), runtimeData.OnGridL1Voltage)
assert.Equal(t, inverter.Current(12.3), runtimeData.OnGridL1Current)
assert.Equal(t, inverter.Frequency(49.99), runtimeData.OnGridL1Frequency)
assert.Equal(t, inverter.Power(2945), runtimeData.OnGridL1Power)
assert.Equal(t, inverter.Voltage(-0.1), runtimeData.OnGridL2Voltage)
assert.Equal(t, inverter.Current(-0.1), runtimeData.OnGridL2Current)
assert.Equal(t, inverter.Frequency(-0.01), runtimeData.OnGridL2Frequency)
assert.Equal(t, inverter.Power(2.147483647e+09), runtimeData.OnGridL2Power)
assert.Equal(t, inverter.Voltage(-0.1), runtimeData.OnGridL3Voltage)
assert.Equal(t, inverter.Current(-0.1), runtimeData.OnGridL3Current)
assert.Equal(t, inverter.Frequency(-0.01), runtimeData.OnGridL3Frequency)
assert.Equal(t, inverter.Power(2.147483647e+09), runtimeData.OnGridL3Power)
assert.Equal(t, 1, runtimeData.GridMode)
assert.Equal(t, inverter.Power(2945), runtimeData.TotalInverterPower)
assert.Equal(t, inverter.Power(1005), runtimeData.ActivePower)
assert.Equal(t, 2147483647, runtimeData.ReactivePower)
assert.Equal(t, -1, runtimeData.ApparentPower)
assert.Equal(t, inverter.Voltage(236.6), runtimeData.BackupL1Voltage)
assert.Equal(t, inverter.Current(0.5), runtimeData.BackupL1Current)
assert.Equal(t, inverter.Frequency(49.99), runtimeData.BackupL1Frequency)
assert.Equal(t, 1, runtimeData.LoadModeL1)
assert.Equal(t, inverter.Power(0), runtimeData.BackupL1Power)
assert.Equal(t, inverter.Voltage(-0.1), runtimeData.BackupL2Voltage)
assert.Equal(t, inverter.Current(-0.1), runtimeData.BackupL2Current)
assert.Equal(t, inverter.Frequency(-0.01), runtimeData.BackupL2Frequency)
assert.Equal(t, -1, runtimeData.LoadModeL2)
assert.Equal(t, inverter.Power(-1), runtimeData.BackupL2Power)
assert.Equal(t, inverter.Voltage(-0.1), runtimeData.BackupL3Voltage)
assert.Equal(t, inverter.Current(-0.1), runtimeData.BackupL3Current)
assert.Equal(t, inverter.Frequency(-0.01), runtimeData.BackupL3Frequency)
assert.Equal(t, -1, runtimeData.LoadModeL3)
assert.Equal(t, inverter.Power(-1), runtimeData.BackupL3Power)
assert.Equal(t, inverter.Power(1940), runtimeData.LoadL1)
assert.Equal(t, inverter.Power(-2.147483648e+09), runtimeData.LoadL2)
assert.Equal(t, inverter.Power(-2.147483648e+09), runtimeData.LoadL3)
assert.Equal(t, inverter.Power(0), runtimeData.BackupLoad)
assert.Equal(t, inverter.Power(1940), runtimeData.Load)
assert.Equal(t, 2, runtimeData.UPSLoad)
assert.Equal(t, inverter.Temp(63.1), runtimeData.TemperatureAir)
assert.Equal(t, inverter.Temp(3276.7), runtimeData.TemperatureModule)
assert.Equal(t, inverter.Temp(40.4), runtimeData.Temperature)
assert.Equal(t, 256, runtimeData.FunctionBit)
assert.Equal(t, inverter.Voltage(370.2), runtimeData.BusVoltage)
assert.Equal(t, inverter.Voltage(-0.1), runtimeData.NBusVoltage)
assert.Equal(t, inverter.Voltage(0), runtimeData.BatteryVoltage)
assert.Equal(t, inverter.Current(0), runtimeData.BatteryCurrent)
assert.Equal(t, 0, runtimeData.BatteryMode)
assert.Equal(t, 0, runtimeData.WarningCode)
assert.Equal(t, 33, runtimeData.SafetyCountryCode)
assert.Equal(t, 131071, runtimeData.WorkMode)
assert.Equal(t, 0, runtimeData.OperationCode)
assert.Equal(t, 0, runtimeData.ErrorCodes)
assert.Equal(t, inverter.Energy(769.9), runtimeData.EnergyGenerationTotal)
assert.Equal(t, inverter.Energy(4.4), runtimeData.EnergyGenerationToday)
assert.Equal(t, inverter.Energy(769.9), runtimeData.EnergyExportTotal)
assert.Equal(t, 271, runtimeData.EnergyExportTotalHours)
assert.Equal(t, inverter.Energy(4.6), runtimeData.EnergyExportToday)
assert.Equal(t, inverter.Energy(0), runtimeData.EnergyImportTotal)
assert.Equal(t, inverter.Energy(0), runtimeData.EnergyImportToday)
assert.Equal(t, inverter.Energy(301.1), runtimeData.EnergyLoadTotal)
assert.Equal(t, inverter.Energy(4.8), runtimeData.EnergyLoadDay)
assert.Equal(t, 0, runtimeData.BatteryChargeTotal)
assert.Equal(t, 0, runtimeData.BatteryChargeToday)
assert.Equal(t, 0, runtimeData.BatteryDischargeTotal)
assert.Equal(t, 0, runtimeData.BatteryDischargeToday)
assert.Equal(t, 34095175, runtimeData.DiagStatusCode)
assert.Equal(t, inverter.Power(1888), runtimeData.HouseConsumption)
})
t.Run("with single-phase inverter", func(t *testing.T) {
inv := inverter.ET{SerialNumber: "EHUfoo"}
runtimeData, err := inv.DecodeRuntimeData(inBytes)
require.NoError(t, err)
assert.Equal(t, inverter.Voltage(316.4), runtimeData.PV1Voltage)
assert.Equal(t, inverter.Current(3.2), runtimeData.PV1Current)
assert.Equal(t, inverter.Power(1012), runtimeData.PV1Power)
assert.Equal(t, inverter.Voltage(227.2), runtimeData.PV2Voltage)
assert.Equal(t, inverter.Current(8.3), runtimeData.PV2Current)
assert.Equal(t, inverter.Power(1881), runtimeData.PV2Power)
assert.Equal(t, inverter.Power(2893), runtimeData.PVPower)
assert.Equal(t, inverter.Voltage(236.7), runtimeData.OnGridL1Voltage)
assert.Equal(t, inverter.Current(12.3), runtimeData.OnGridL1Current)
assert.Equal(t, inverter.Frequency(49.99), runtimeData.OnGridL1Frequency)
assert.Equal(t, inverter.Power(2945), runtimeData.OnGridL1Power)
assert.Equal(t, inverter.Voltage(0), runtimeData.OnGridL2Voltage)
assert.Equal(t, inverter.Current(0), runtimeData.OnGridL2Current)
assert.Equal(t, inverter.Frequency(0), runtimeData.OnGridL2Frequency)
assert.Equal(t, inverter.Power(0), runtimeData.OnGridL2Power)
assert.Equal(t, inverter.Voltage(0), runtimeData.OnGridL3Voltage)
assert.Equal(t, inverter.Current(0), runtimeData.OnGridL3Current)
assert.Equal(t, inverter.Frequency(0), runtimeData.OnGridL3Frequency)
assert.Equal(t, inverter.Power(0), runtimeData.OnGridL3Power)
assert.Equal(t, inverter.Voltage(236.6), runtimeData.BackupL1Voltage)
assert.Equal(t, inverter.Current(0.5), runtimeData.BackupL1Current)
assert.Equal(t, inverter.Frequency(49.99), runtimeData.BackupL1Frequency)
assert.Equal(t, 1, runtimeData.LoadModeL1)
assert.Equal(t, inverter.Power(0), runtimeData.BackupL1Power)
assert.Equal(t, inverter.Voltage(0), runtimeData.BackupL2Voltage)
assert.Equal(t, inverter.Current(0), runtimeData.BackupL2Current)
assert.Equal(t, inverter.Frequency(0), runtimeData.BackupL2Frequency)
assert.Equal(t, 0, runtimeData.LoadModeL2)
assert.Equal(t, inverter.Power(0), runtimeData.BackupL2Power)
assert.Equal(t, inverter.Voltage(0), runtimeData.BackupL3Voltage)
assert.Equal(t, inverter.Current(0), runtimeData.BackupL3Current)
assert.Equal(t, inverter.Frequency(0), runtimeData.BackupL3Frequency)
assert.Equal(t, 0, runtimeData.LoadModeL3)
assert.Equal(t, inverter.Power(1940), runtimeData.LoadL1)
assert.Equal(t, inverter.Power(0), runtimeData.LoadL2)
assert.Equal(t, inverter.Power(0), runtimeData.LoadL3)
})
}

View File

@ -1,5 +1,45 @@
package inverter package inverter
import (
"fmt"
"time"
"golang.org/x/exp/constraints"
)
type numeric interface {
constraints.Integer | constraints.Float
}
type (
Power float64
Voltage float64
Current float64
Energy float64
Frequency float64
Temp float64
)
func newPower[T numeric](v T) Power { return Power(float64(v)) }
func newVoltage[T numeric](v T) Voltage { return Voltage(float64(v) / 10.0) }
func newCurrent[T numeric](v T) Current { return Current(float64(v) / 10.0) }
func newFrequency[T numeric](v T) Frequency { return Frequency(float64(v) / 100.0) }
func newTemp(v int16) Temp { return Temp(float64(v) / 10.0) }
func newEnergy[T numeric](v T) Energy {
f := float64(v)
if f == -1 {
return 0
}
return Energy(f / 10.0)
}
func (v Power) String() string { return fmt.Sprintf("%f W", v) }
func (v Voltage) String() string { return fmt.Sprintf("%f V", v) }
func (v Current) String() string { return fmt.Sprintf("%f A", v) }
func (v Energy) String() string { return fmt.Sprintf("%f kWh", v) }
func (v Frequency) String() string { return fmt.Sprintf("%f Hz", v) }
func (v Temp) String() string { return fmt.Sprintf("%f C", v) }
// DeviceInfo holds the static information about an inverter. // DeviceInfo holds the static information about an inverter.
type DeviceInfo struct { type DeviceInfo struct {
ModbusVersion int `json:"modbus_version"` ModbusVersion int `json:"modbus_version"`
@ -14,84 +54,85 @@ type DeviceInfo struct {
ArmSVNVersion int `json:"arm_svn_version"` ArmSVNVersion int `json:"arm_svn_version"`
SoftwareVersion string `json:"software_version"` SoftwareVersion string `json:"software_version"`
ArmVersion string `json:"arm_version"` ArmVersion string `json:"arm_version"`
SinglePhase bool SinglePhase bool `json:"single_phase"`
} }
type ETRuntimeData struct { type ETRuntimeData struct {
PV1Voltage int `json:"pv1_voltage"` Timestamp time.Time `json:"timestamp"`
PV1Current int `json:"pv1_current"` PV1Voltage Voltage `json:"pv1_voltage"`
PV1Power int `json:"pv1_power"` PV1Current Current `json:"pv1_current"`
PV2Voltage int `json:"pv2_voltage"` PV1Power Power `json:"pv1_power"`
PV2Current int `json:"pv2_current"` PV2Voltage Voltage `json:"pv2_voltage"`
PV2Power int `json:"pv2_power"` PV2Current Current `json:"pv2_current"`
PVPower int `json:"pv_power"` PV2Power Power `json:"pv2_power"`
PV2Mode byte `json:"pv2_mode"` PVPower Power `json:"pv_power"`
PV1Mode byte `json:"pv1_mode"` PV2Mode byte `json:"pv2_mode"`
OnGridL1Voltage int `json:"on_grid_l1_voltage"` PV1Mode byte `json:"pv1_mode"`
OnGridL1Current int `json:"on_grid_l1_current"` OnGridL1Voltage Voltage `json:"on_grid_l1_voltage"`
OnGridL1Frequency int `json:"on_grid_l1_frequency"` OnGridL1Current Current `json:"on_grid_l1_current"`
OnGridL1Power int `json:"on_grid_l1_power"` OnGridL1Frequency Frequency `json:"on_grid_l1_frequency"`
OnGridL2Voltage int `json:"on_grid_l2_voltage"` OnGridL1Power Power `json:"on_grid_l1_power"`
OnGridL2Current int `json:"on_grid_l2_current"` OnGridL2Voltage Voltage `json:"on_grid_l2_voltage"`
OnGridL2Frequency int `json:"on_grid_l2_frequency"` OnGridL2Current Current `json:"on_grid_l2_current"`
OnGridL2Power int `json:"on_grid_l2_power"` OnGridL2Frequency Frequency `json:"on_grid_l2_frequency"`
OnGridL3Voltage int `json:"on_grid_l3_voltage"` OnGridL2Power Power `json:"on_grid_l2_power"`
OnGridL3Current int `json:"on_grid_l3_current"` OnGridL3Voltage Voltage `json:"on_grid_l3_voltage"`
OnGridL3Frequency int `json:"on_grid_l3_frequency"` OnGridL3Current Current `json:"on_grid_l3_current"`
OnGridL3Power int `json:"on_grid_l3_power"` OnGridL3Frequency Frequency `json:"on_grid_l3_frequency"`
GridMode int `json:"grid_mode"` OnGridL3Power Power `json:"on_grid_l3_power"`
TotalInverterPower int `json:"total_inverter_power"` GridMode int `json:"grid_mode"`
ActivePower int `json:"active_power"` TotalInverterPower Power `json:"total_inverter_power"`
ReactivePower int `json:"reactive_power"` ActivePower Power `json:"active_power"`
ApparentPower int `json:"apparent_power"` ReactivePower int `json:"reactive_power"`
BackupL1Voltage int `json:"backup_l1_voltage"` ApparentPower int `json:"apparent_power"`
BackupL1Current int `json:"backup_l1_current"` BackupL1Voltage Voltage `json:"backup_l1_voltage"`
BackupL1Frequency int `json:"backup_l1_frequency"` BackupL1Current Current `json:"backup_l1_current"`
LoadModeL1 int `json:"load_mode_l1"` BackupL1Frequency Frequency `json:"backup_l1_frequency"`
BackupL1Power int `json:"backup_l1_power"` LoadModeL1 int `json:"load_mode_l1"`
BackupL2Voltage int `json:"backup_l2_voltage"` BackupL1Power Power `json:"backup_l1_power"`
BackupL2Current int `json:"backup_l2_current"` BackupL2Voltage Voltage `json:"backup_l2_voltage"`
BackupL2Frequency int `json:"backup_l2_frequency"` BackupL2Current Current `json:"backup_l2_current"`
LoadModeL2 int `json:"load_mode_l2"` BackupL2Frequency Frequency `json:"backup_l2_frequency"`
BackupL2Power int `json:"backup_l2_power"` LoadModeL2 int `json:"load_mode_l2"`
BackupL3Voltage int `json:"backup_l3_voltage"` BackupL2Power Power `json:"backup_l2_power"`
BackupL3Current int `json:"backup_l3_current"` BackupL3Voltage Voltage `json:"backup_l3_voltage"`
BackupL3Frequency int `json:"backup_l3_frequency"` BackupL3Current Current `json:"backup_l3_current"`
LoadModeL3 int `json:"load_mode_l3"` BackupL3Frequency Frequency `json:"backup_l3_frequency"`
BackupL3Power int `json:"backup_l3_power"` LoadModeL3 int `json:"load_mode_l3"`
LoadL1 int `json:"load_l1"` BackupL3Power Power `json:"backup_l3_power"`
LoadL2 int `json:"load_l2"` LoadL1 Power `json:"load_l1"`
LoadL3 int `json:"load_l3"` LoadL2 Power `json:"load_l2"`
BackupLoad int `json:"backup_load"` LoadL3 Power `json:"load_l3"`
Load int `json:"load"` BackupLoad Power `json:"backup_load"`
UPSLoad int `json:"ups_load"` Load Power `json:"load"`
TemperatureAir int `json:"temperature_air"` UPSLoad int `json:"ups_load"`
TemperatureModule int `json:"temperature_module"` TemperatureAir Temp `json:"temperature_air"`
Temperature int `json:"temperature"` TemperatureModule Temp `json:"temperature_module"`
FunctionBit int `json:"-"` Temperature Temp `json:"temperature"`
BusVoltage int `json:"bus_voltage"` FunctionBit int `json:"-"`
NBusVoltage int `json:"nbus_voltage"` BusVoltage Voltage `json:"bus_voltage"`
BatteryVoltage int `json:"battery_voltage"` NBusVoltage Voltage `json:"nbus_voltage"`
BatteryCurrent int `json:"battery_current"` BatteryVoltage Voltage `json:"battery_voltage"`
BatteryMode int `json:"battery_mode"` BatteryCurrent Current `json:"battery_current"`
WarningCode int `json:"warning_code"` BatteryMode int `json:"battery_mode"`
SafetyCountryCode int `json:"safety_country_code"` WarningCode int `json:"warning_code"`
WorkMode int `json:"work_mode"` SafetyCountryCode int `json:"safety_country_code"`
OperationCode int `json:"operation_code"` WorkMode int `json:"work_mode"`
ErrorCodes int `json:"-"` OperationCode int `json:"operation_code"`
PVGenerationTotal int `json:"pv_generation_total"` ErrorCodes int `json:"-"`
PVGenerationToday int `json:"pv_generation_today"` EnergyGenerationTotal Energy `json:"energy_generation_total"`
EnergyExportTotal int `json:"energy_export_total"` EnergyGenerationToday Energy `json:"energy_generation_today"`
EnergyExportTotalHours int `json:"energy_export_total_hours"` EnergyExportTotal Energy `json:"energy_export_total"`
EnergyExportToday int `json:"energy_export_today"` EnergyExportTotalHours int `json:"energy_export_total_hours"`
EnergyImportTotal int `json:"energy_import_total"` EnergyExportToday Energy `json:"energy_export_today"`
EnergyImportToday int `json:"energy_import_today"` EnergyImportTotal Energy `json:"energy_import_total"`
EnergyLoadTotal int `json:"energy_load_total"` EnergyImportToday Energy `json:"energy_import_today"`
EnergyLoadDay int `json:"energy_load_day"` EnergyLoadTotal Energy `json:"energy_load_total"`
BatteryChargeTotal int `json:"battery_charge_total"` EnergyLoadDay Energy `json:"energy_load_day"`
BatteryChargeToday int `json:"battery_charge_today"` BatteryChargeTotal int `json:"battery_charge_total"`
BatteryDischargeTotal int `json:"battery_discharge_total"` BatteryChargeToday int `json:"battery_charge_today"`
BatteryDischargeToday int `json:"battery_discharge_today"` BatteryDischargeTotal int `json:"battery_discharge_total"`
DiagStatusCode int `json:"-"` BatteryDischargeToday int `json:"battery_discharge_today"`
HouseConsumption int32 `json:"house_consumption"` DiagStatusCode int `json:"-"`
HouseConsumption Power `json:"house_consumption"`
} }