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 (data *etRuntimeData) toRuntimeData(singlePhase bool) *ETRuntimeData { func filterSinglePhase[T numeric](v T, singlePhase bool) T {
filterSinglePhase := func(i int) int {
if singlePhase { if singlePhase {
return 0 return 0
} }
return i return v
}
// toRuntimeData panics if the `locationName` constant cannot be resolved to a
// time.Location.
func (data *etRuntimeData) toRuntimeData(singlePhase bool) *ETRuntimeData {
yr := data.Timestamp[0]
mon := data.Timestamp[1]
day := data.Timestamp[2]
hr := data.Timestamp[3]
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"`
PVPower Power `json:"pv_power"`
PV2Mode byte `json:"pv2_mode"` PV2Mode byte `json:"pv2_mode"`
PV1Mode byte `json:"pv1_mode"` PV1Mode byte `json:"pv1_mode"`
OnGridL1Voltage int `json:"on_grid_l1_voltage"` OnGridL1Voltage Voltage `json:"on_grid_l1_voltage"`
OnGridL1Current int `json:"on_grid_l1_current"` OnGridL1Current Current `json:"on_grid_l1_current"`
OnGridL1Frequency int `json:"on_grid_l1_frequency"` OnGridL1Frequency Frequency `json:"on_grid_l1_frequency"`
OnGridL1Power int `json:"on_grid_l1_power"` OnGridL1Power Power `json:"on_grid_l1_power"`
OnGridL2Voltage int `json:"on_grid_l2_voltage"` OnGridL2Voltage Voltage `json:"on_grid_l2_voltage"`
OnGridL2Current int `json:"on_grid_l2_current"` OnGridL2Current Current `json:"on_grid_l2_current"`
OnGridL2Frequency int `json:"on_grid_l2_frequency"` OnGridL2Frequency Frequency `json:"on_grid_l2_frequency"`
OnGridL2Power int `json:"on_grid_l2_power"` OnGridL2Power Power `json:"on_grid_l2_power"`
OnGridL3Voltage int `json:"on_grid_l3_voltage"` OnGridL3Voltage Voltage `json:"on_grid_l3_voltage"`
OnGridL3Current int `json:"on_grid_l3_current"` OnGridL3Current Current `json:"on_grid_l3_current"`
OnGridL3Frequency int `json:"on_grid_l3_frequency"` OnGridL3Frequency Frequency `json:"on_grid_l3_frequency"`
OnGridL3Power int `json:"on_grid_l3_power"` OnGridL3Power Power `json:"on_grid_l3_power"`
GridMode int `json:"grid_mode"` GridMode int `json:"grid_mode"`
TotalInverterPower int `json:"total_inverter_power"` TotalInverterPower Power `json:"total_inverter_power"`
ActivePower int `json:"active_power"` ActivePower Power `json:"active_power"`
ReactivePower int `json:"reactive_power"` ReactivePower int `json:"reactive_power"`
ApparentPower int `json:"apparent_power"` ApparentPower int `json:"apparent_power"`
BackupL1Voltage int `json:"backup_l1_voltage"` BackupL1Voltage Voltage `json:"backup_l1_voltage"`
BackupL1Current int `json:"backup_l1_current"` BackupL1Current Current `json:"backup_l1_current"`
BackupL1Frequency int `json:"backup_l1_frequency"` BackupL1Frequency Frequency `json:"backup_l1_frequency"`
LoadModeL1 int `json:"load_mode_l1"` LoadModeL1 int `json:"load_mode_l1"`
BackupL1Power int `json:"backup_l1_power"` BackupL1Power Power `json:"backup_l1_power"`
BackupL2Voltage int `json:"backup_l2_voltage"` BackupL2Voltage Voltage `json:"backup_l2_voltage"`
BackupL2Current int `json:"backup_l2_current"` BackupL2Current Current `json:"backup_l2_current"`
BackupL2Frequency int `json:"backup_l2_frequency"` BackupL2Frequency Frequency `json:"backup_l2_frequency"`
LoadModeL2 int `json:"load_mode_l2"` LoadModeL2 int `json:"load_mode_l2"`
BackupL2Power int `json:"backup_l2_power"` BackupL2Power Power `json:"backup_l2_power"`
BackupL3Voltage int `json:"backup_l3_voltage"` BackupL3Voltage Voltage `json:"backup_l3_voltage"`
BackupL3Current int `json:"backup_l3_current"` BackupL3Current Current `json:"backup_l3_current"`
BackupL3Frequency int `json:"backup_l3_frequency"` BackupL3Frequency Frequency `json:"backup_l3_frequency"`
LoadModeL3 int `json:"load_mode_l3"` LoadModeL3 int `json:"load_mode_l3"`
BackupL3Power int `json:"backup_l3_power"` BackupL3Power Power `json:"backup_l3_power"`
LoadL1 int `json:"load_l1"` LoadL1 Power `json:"load_l1"`
LoadL2 int `json:"load_l2"` LoadL2 Power `json:"load_l2"`
LoadL3 int `json:"load_l3"` LoadL3 Power `json:"load_l3"`
BackupLoad int `json:"backup_load"` BackupLoad Power `json:"backup_load"`
Load int `json:"load"` Load Power `json:"load"`
UPSLoad int `json:"ups_load"` UPSLoad int `json:"ups_load"`
TemperatureAir int `json:"temperature_air"` TemperatureAir Temp `json:"temperature_air"`
TemperatureModule int `json:"temperature_module"` TemperatureModule Temp `json:"temperature_module"`
Temperature int `json:"temperature"` Temperature Temp `json:"temperature"`
FunctionBit int `json:"-"` FunctionBit int `json:"-"`
BusVoltage int `json:"bus_voltage"` BusVoltage Voltage `json:"bus_voltage"`
NBusVoltage int `json:"nbus_voltage"` NBusVoltage Voltage `json:"nbus_voltage"`
BatteryVoltage int `json:"battery_voltage"` BatteryVoltage Voltage `json:"battery_voltage"`
BatteryCurrent int `json:"battery_current"` BatteryCurrent Current `json:"battery_current"`
BatteryMode int `json:"battery_mode"` BatteryMode int `json:"battery_mode"`
WarningCode int `json:"warning_code"` WarningCode int `json:"warning_code"`
SafetyCountryCode int `json:"safety_country_code"` SafetyCountryCode int `json:"safety_country_code"`
WorkMode int `json:"work_mode"` WorkMode int `json:"work_mode"`
OperationCode int `json:"operation_code"` OperationCode int `json:"operation_code"`
ErrorCodes int `json:"-"` ErrorCodes int `json:"-"`
PVGenerationTotal int `json:"pv_generation_total"` EnergyGenerationTotal Energy `json:"energy_generation_total"`
PVGenerationToday int `json:"pv_generation_today"` EnergyGenerationToday Energy `json:"energy_generation_today"`
EnergyExportTotal int `json:"energy_export_total"` EnergyExportTotal Energy `json:"energy_export_total"`
EnergyExportTotalHours int `json:"energy_export_total_hours"` EnergyExportTotalHours int `json:"energy_export_total_hours"`
EnergyExportToday int `json:"energy_export_today"` EnergyExportToday Energy `json:"energy_export_today"`
EnergyImportTotal int `json:"energy_import_total"` EnergyImportTotal Energy `json:"energy_import_total"`
EnergyImportToday int `json:"energy_import_today"` EnergyImportToday Energy `json:"energy_import_today"`
EnergyLoadTotal int `json:"energy_load_total"` EnergyLoadTotal Energy `json:"energy_load_total"`
EnergyLoadDay int `json:"energy_load_day"` EnergyLoadDay Energy `json:"energy_load_day"`
BatteryChargeTotal int `json:"battery_charge_total"` BatteryChargeTotal int `json:"battery_charge_total"`
BatteryChargeToday int `json:"battery_charge_today"` BatteryChargeToday int `json:"battery_charge_today"`
BatteryDischargeTotal int `json:"battery_discharge_total"` BatteryDischargeTotal int `json:"battery_discharge_total"`
BatteryDischargeToday int `json:"battery_discharge_today"` BatteryDischargeToday int `json:"battery_discharge_today"`
DiagStatusCode int `json:"-"` DiagStatusCode int `json:"-"`
HouseConsumption int32 `json:"house_consumption"` HouseConsumption Power `json:"house_consumption"`
} }