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
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"
"math"
"strings"
"time"
"git.netflux.io/rob/goodwe-go/command"
)
// The timezone used to parse timestamps.
const locationName = "Europe/Madrid"
type ET struct {
SerialNumber string
ModelName string
}
func (inv ET) isSinglePhase() bool {
return strings.Contains(inv.SerialNumber, "EHU")
}
// Unexported struct used for parsing binary data only.
type etDeviceInfo struct {
ModbusVersion uint16
@ -53,8 +61,20 @@ func (info *etDeviceInfo) toDeviceInfo() *DeviceInfo {
}
// 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 {
_ [6]byte
Timestamp [6]byte
PV1Voltage int16
PV1Current int16
PV1Power int32
@ -117,8 +137,8 @@ type etRuntimeData struct {
WorkMode int32
OperationCode int16
ErrorCodes int16
PVGenerationTotal int32
PVGenerationToday int32
EnergyGenerationTotal int32
EnergyGenerationToday int32
EnergyExportTotal int32
EnergyExportTotalHours int32
EnergyExportToday int16
@ -134,94 +154,118 @@ type etRuntimeData struct {
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 {
filterSinglePhase := func(i int) int {
if singlePhase {
return 0
}
return i
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{
PV1Voltage: int(data.PV1Voltage),
PV1Current: int(data.PV1Current),
PV1Power: int(data.PV1Power),
PV2Voltage: int(data.PV2Voltage),
PV2Current: int(data.PV2Current),
PV2Power: int(data.PV2Power),
PVPower: int(data.PV1Power) + int(data.PV2Power),
Timestamp: time.Date(2000+int(yr), time.Month(mon), int(day), int(hr), int(min), int(sec), 0, loc),
PV1Voltage: newVoltage(data.PV1Voltage),
PV1Current: newCurrent(data.PV1Current),
PV1Power: newPower(data.PV1Power),
PV2Voltage: newVoltage(data.PV2Voltage),
PV2Current: newCurrent(data.PV2Current),
PV2Power: newPower(data.PV2Power),
PVPower: newPower(data.PV1Power + data.PV2Power),
PV2Mode: data.PV2Mode,
PV1Mode: data.PV1Mode,
OnGridL1Voltage: int(data.OnGridL1Voltage),
OnGridL1Current: int(data.OnGridL1Current),
OnGridL1Frequency: int(data.OnGridL1Frequency),
OnGridL1Power: int(data.OnGridL1Power),
OnGridL2Voltage: filterSinglePhase(int(data.OnGridL2Voltage)),
OnGridL2Current: filterSinglePhase(int(data.OnGridL2Current)),
OnGridL2Frequency: filterSinglePhase(int(data.OnGridL2Frequency)),
OnGridL2Power: filterSinglePhase(int(data.OnGridL2Power)),
OnGridL3Voltage: filterSinglePhase(int(data.OnGridL3Voltage)),
OnGridL3Current: filterSinglePhase(int(data.OnGridL3Current)),
OnGridL3Frequency: filterSinglePhase(int(data.OnGridL3Frequency)),
OnGridL3Power: filterSinglePhase(int(data.OnGridL3Power)),
OnGridL1Voltage: newVoltage(data.OnGridL1Voltage),
OnGridL1Current: newCurrent(data.OnGridL1Current),
OnGridL1Frequency: newFrequency(data.OnGridL1Frequency),
OnGridL1Power: newPower(data.OnGridL1Power),
OnGridL2Voltage: newVoltage(filterSinglePhase(data.OnGridL2Voltage, singlePhase)),
OnGridL2Current: newCurrent(filterSinglePhase(data.OnGridL2Current, singlePhase)),
OnGridL2Frequency: newFrequency(filterSinglePhase(data.OnGridL2Frequency, singlePhase)),
OnGridL2Power: newPower(filterSinglePhase(data.OnGridL2Power, singlePhase)),
OnGridL3Voltage: newVoltage(filterSinglePhase(data.OnGridL3Voltage, singlePhase)),
OnGridL3Current: newCurrent(filterSinglePhase(data.OnGridL3Current, singlePhase)),
OnGridL3Frequency: newFrequency(filterSinglePhase(data.OnGridL3Frequency, singlePhase)),
OnGridL3Power: newPower(filterSinglePhase(data.OnGridL3Power, singlePhase)),
GridMode: int(data.GridMode),
TotalInverterPower: int(data.TotalInverterPower),
ActivePower: int(data.ActivePower),
TotalInverterPower: newPower(data.TotalInverterPower),
ActivePower: newPower(data.ActivePower),
ReactivePower: int(data.ReactivePower),
ApparentPower: int(data.ApparentPower),
BackupL1Voltage: int(data.BackupL1Voltage),
BackupL1Current: int(data.BackupL1Current),
BackupL1Frequency: int(data.BackupL1Frequency),
BackupL1Voltage: newVoltage(data.BackupL1Voltage),
BackupL1Current: newCurrent(data.BackupL1Current),
BackupL1Frequency: newFrequency(data.BackupL1Frequency),
LoadModeL1: int(data.LoadModeL1),
BackupL1Power: int(data.BackupL1Power),
BackupL2Voltage: filterSinglePhase(int(data.BackupL2Voltage)),
BackupL2Current: filterSinglePhase(int(data.BackupL2Current)),
BackupL2Frequency: filterSinglePhase(int(data.BackupL2Frequency)),
LoadModeL2: filterSinglePhase(int(data.LoadModeL2)),
BackupL2Power: filterSinglePhase(int(data.BackupL2Power)),
BackupL3Voltage: filterSinglePhase(int(data.BackupL3Voltage)),
BackupL3Current: filterSinglePhase(int(data.BackupL3Current)),
BackupL3Frequency: filterSinglePhase(int(data.BackupL3Frequency)),
LoadModeL3: filterSinglePhase(int(data.LoadModeL3)),
BackupL3Power: filterSinglePhase(int(data.BackupL3Power)),
LoadL1: int(data.LoadL1),
LoadL2: filterSinglePhase(int(data.LoadL2)),
LoadL3: filterSinglePhase(int(data.LoadL3)),
BackupLoad: int(data.BackupLoad),
Load: int(data.Load),
BackupL1Power: newPower(data.BackupL1Power),
BackupL2Voltage: newVoltage(filterSinglePhase(data.BackupL2Voltage, singlePhase)),
BackupL2Current: newCurrent(filterSinglePhase(data.BackupL2Current, singlePhase)),
BackupL2Frequency: newFrequency(filterSinglePhase(data.BackupL2Frequency, singlePhase)),
LoadModeL2: int(filterSinglePhase(data.LoadModeL2, singlePhase)),
BackupL2Power: newPower(filterSinglePhase(data.BackupL2Power, singlePhase)),
BackupL3Voltage: newVoltage(filterSinglePhase(data.BackupL3Voltage, singlePhase)),
BackupL3Current: newCurrent(filterSinglePhase(data.BackupL3Current, singlePhase)),
BackupL3Frequency: newFrequency(filterSinglePhase(data.BackupL3Frequency, singlePhase)),
LoadModeL3: int(filterSinglePhase(data.LoadModeL3, singlePhase)),
BackupL3Power: newPower(filterSinglePhase(data.BackupL3Power, singlePhase)),
LoadL1: newPower(data.LoadL1),
LoadL2: newPower(filterSinglePhase(data.LoadL2, singlePhase)),
LoadL3: newPower(filterSinglePhase(data.LoadL3, singlePhase)),
BackupLoad: newPower(data.BackupLoad),
Load: newPower(data.Load),
UPSLoad: int(data.UPSLoad),
TemperatureAir: int(data.TemperatureAir),
TemperatureModule: int(data.TemperatureModule),
Temperature: int(data.Temperature),
TemperatureAir: newTemp(data.TemperatureAir),
TemperatureModule: newTemp(data.TemperatureModule),
Temperature: newTemp(data.Temperature),
FunctionBit: int(data.FunctionBit),
BusVoltage: int(data.BusVoltage),
NBusVoltage: int(data.NBusVoltage),
BatteryVoltage: int(data.BatteryVoltage),
BatteryCurrent: int(data.BatteryCurrent),
BusVoltage: newVoltage(data.BusVoltage),
NBusVoltage: newVoltage(data.NBusVoltage),
BatteryVoltage: newVoltage(data.BatteryVoltage),
BatteryCurrent: newCurrent(data.BatteryCurrent),
BatteryMode: int(data.BatteryMode),
WarningCode: int(data.WarningCode),
SafetyCountryCode: int(data.SafetyCountryCode),
WorkMode: int(data.WorkMode),
OperationCode: int(data.OperationCode),
ErrorCodes: int(data.ErrorCodes),
PVGenerationTotal: int(data.PVGenerationTotal),
PVGenerationToday: int(data.PVGenerationToday),
EnergyExportTotal: int(data.EnergyExportTotal),
EnergyGenerationTotal: newEnergy(data.EnergyGenerationTotal),
EnergyGenerationToday: newEnergy(data.EnergyGenerationToday),
EnergyExportTotal: newEnergy(data.EnergyExportTotal),
EnergyExportTotalHours: int(data.EnergyExportTotalHours),
EnergyExportToday: int(data.EnergyExportToday),
EnergyImportTotal: int(data.EnergyImportTotal),
EnergyImportToday: int(data.EnergyImportToday),
EnergyLoadTotal: int(data.EnergyLoadTotal),
EnergyLoadDay: int(data.EnergyLoadDay),
EnergyExportToday: newEnergy(data.EnergyExportToday),
EnergyImportTotal: newEnergy(data.EnergyImportTotal),
EnergyImportToday: newEnergy(data.EnergyImportToday),
EnergyLoadTotal: newEnergy(data.EnergyLoadTotal),
EnergyLoadDay: newEnergy(data.EnergyLoadDay),
BatteryChargeTotal: int(data.BatteryChargeTotal),
BatteryChargeToday: int(data.BatteryChargeToday),
BatteryDischargeTotal: int(data.BatteryDischargeTotal),
BatteryDischargeToday: int(data.BatteryDischargeToday),
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) {
resp, err := command.Send(command.NewModbus(command.ModbusCommandTypeRead, 0x88b8, 0x0021), conn)
if err != nil {
@ -236,6 +280,7 @@ func (inv ET) DeviceInfo(ctx context.Context, conn io.ReadWriter) (*DeviceInfo,
return deviceInfo.toDeviceInfo(), nil
}
// DEPRECATED
func (inv ET) RuntimeData(ctx context.Context, conn io.ReadWriter) (*ETRuntimeData, error) {
deviceInfo, err := inv.DeviceInfo(ctx, conn)
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
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.
type DeviceInfo struct {
ModbusVersion int `json:"modbus_version"`
@ -14,84 +54,85 @@ type DeviceInfo struct {
ArmSVNVersion int `json:"arm_svn_version"`
SoftwareVersion string `json:"software_version"`
ArmVersion string `json:"arm_version"`
SinglePhase bool
SinglePhase bool `json:"single_phase"`
}
type ETRuntimeData struct {
PV1Voltage int `json:"pv1_voltage"`
PV1Current int `json:"pv1_current"`
PV1Power int `json:"pv1_power"`
PV2Voltage int `json:"pv2_voltage"`
PV2Current int `json:"pv2_current"`
PV2Power int `json:"pv2_power"`
PVPower int `json:"pv_power"`
PV2Mode byte `json:"pv2_mode"`
PV1Mode byte `json:"pv1_mode"`
OnGridL1Voltage int `json:"on_grid_l1_voltage"`
OnGridL1Current int `json:"on_grid_l1_current"`
OnGridL1Frequency int `json:"on_grid_l1_frequency"`
OnGridL1Power int `json:"on_grid_l1_power"`
OnGridL2Voltage int `json:"on_grid_l2_voltage"`
OnGridL2Current int `json:"on_grid_l2_current"`
OnGridL2Frequency int `json:"on_grid_l2_frequency"`
OnGridL2Power int `json:"on_grid_l2_power"`
OnGridL3Voltage int `json:"on_grid_l3_voltage"`
OnGridL3Current int `json:"on_grid_l3_current"`
OnGridL3Frequency int `json:"on_grid_l3_frequency"`
OnGridL3Power int `json:"on_grid_l3_power"`
GridMode int `json:"grid_mode"`
TotalInverterPower int `json:"total_inverter_power"`
ActivePower int `json:"active_power"`
ReactivePower int `json:"reactive_power"`
ApparentPower int `json:"apparent_power"`
BackupL1Voltage int `json:"backup_l1_voltage"`
BackupL1Current int `json:"backup_l1_current"`
BackupL1Frequency int `json:"backup_l1_frequency"`
LoadModeL1 int `json:"load_mode_l1"`
BackupL1Power int `json:"backup_l1_power"`
BackupL2Voltage int `json:"backup_l2_voltage"`
BackupL2Current int `json:"backup_l2_current"`
BackupL2Frequency int `json:"backup_l2_frequency"`
LoadModeL2 int `json:"load_mode_l2"`
BackupL2Power int `json:"backup_l2_power"`
BackupL3Voltage int `json:"backup_l3_voltage"`
BackupL3Current int `json:"backup_l3_current"`
BackupL3Frequency int `json:"backup_l3_frequency"`
LoadModeL3 int `json:"load_mode_l3"`
BackupL3Power int `json:"backup_l3_power"`
LoadL1 int `json:"load_l1"`
LoadL2 int `json:"load_l2"`
LoadL3 int `json:"load_l3"`
BackupLoad int `json:"backup_load"`
Load int `json:"load"`
UPSLoad int `json:"ups_load"`
TemperatureAir int `json:"temperature_air"`
TemperatureModule int `json:"temperature_module"`
Temperature int `json:"temperature"`
FunctionBit int `json:"-"`
BusVoltage int `json:"bus_voltage"`
NBusVoltage int `json:"nbus_voltage"`
BatteryVoltage int `json:"battery_voltage"`
BatteryCurrent int `json:"battery_current"`
BatteryMode int `json:"battery_mode"`
WarningCode int `json:"warning_code"`
SafetyCountryCode int `json:"safety_country_code"`
WorkMode int `json:"work_mode"`
OperationCode int `json:"operation_code"`
ErrorCodes int `json:"-"`
PVGenerationTotal int `json:"pv_generation_total"`
PVGenerationToday int `json:"pv_generation_today"`
EnergyExportTotal int `json:"energy_export_total"`
EnergyExportTotalHours int `json:"energy_export_total_hours"`
EnergyExportToday int `json:"energy_export_today"`
EnergyImportTotal int `json:"energy_import_total"`
EnergyImportToday int `json:"energy_import_today"`
EnergyLoadTotal int `json:"energy_load_total"`
EnergyLoadDay int `json:"energy_load_day"`
BatteryChargeTotal int `json:"battery_charge_total"`
BatteryChargeToday int `json:"battery_charge_today"`
BatteryDischargeTotal int `json:"battery_discharge_total"`
BatteryDischargeToday int `json:"battery_discharge_today"`
DiagStatusCode int `json:"-"`
HouseConsumption int32 `json:"house_consumption"`
Timestamp time.Time `json:"timestamp"`
PV1Voltage Voltage `json:"pv1_voltage"`
PV1Current Current `json:"pv1_current"`
PV1Power Power `json:"pv1_power"`
PV2Voltage Voltage `json:"pv2_voltage"`
PV2Current Current `json:"pv2_current"`
PV2Power Power `json:"pv2_power"`
PVPower Power `json:"pv_power"`
PV2Mode byte `json:"pv2_mode"`
PV1Mode byte `json:"pv1_mode"`
OnGridL1Voltage Voltage `json:"on_grid_l1_voltage"`
OnGridL1Current Current `json:"on_grid_l1_current"`
OnGridL1Frequency Frequency `json:"on_grid_l1_frequency"`
OnGridL1Power Power `json:"on_grid_l1_power"`
OnGridL2Voltage Voltage `json:"on_grid_l2_voltage"`
OnGridL2Current Current `json:"on_grid_l2_current"`
OnGridL2Frequency Frequency `json:"on_grid_l2_frequency"`
OnGridL2Power Power `json:"on_grid_l2_power"`
OnGridL3Voltage Voltage `json:"on_grid_l3_voltage"`
OnGridL3Current Current `json:"on_grid_l3_current"`
OnGridL3Frequency Frequency `json:"on_grid_l3_frequency"`
OnGridL3Power Power `json:"on_grid_l3_power"`
GridMode int `json:"grid_mode"`
TotalInverterPower Power `json:"total_inverter_power"`
ActivePower Power `json:"active_power"`
ReactivePower int `json:"reactive_power"`
ApparentPower int `json:"apparent_power"`
BackupL1Voltage Voltage `json:"backup_l1_voltage"`
BackupL1Current Current `json:"backup_l1_current"`
BackupL1Frequency Frequency `json:"backup_l1_frequency"`
LoadModeL1 int `json:"load_mode_l1"`
BackupL1Power Power `json:"backup_l1_power"`
BackupL2Voltage Voltage `json:"backup_l2_voltage"`
BackupL2Current Current `json:"backup_l2_current"`
BackupL2Frequency Frequency `json:"backup_l2_frequency"`
LoadModeL2 int `json:"load_mode_l2"`
BackupL2Power Power `json:"backup_l2_power"`
BackupL3Voltage Voltage `json:"backup_l3_voltage"`
BackupL3Current Current `json:"backup_l3_current"`
BackupL3Frequency Frequency `json:"backup_l3_frequency"`
LoadModeL3 int `json:"load_mode_l3"`
BackupL3Power Power `json:"backup_l3_power"`
LoadL1 Power `json:"load_l1"`
LoadL2 Power `json:"load_l2"`
LoadL3 Power `json:"load_l3"`
BackupLoad Power `json:"backup_load"`
Load Power `json:"load"`
UPSLoad int `json:"ups_load"`
TemperatureAir Temp `json:"temperature_air"`
TemperatureModule Temp `json:"temperature_module"`
Temperature Temp `json:"temperature"`
FunctionBit int `json:"-"`
BusVoltage Voltage `json:"bus_voltage"`
NBusVoltage Voltage `json:"nbus_voltage"`
BatteryVoltage Voltage `json:"battery_voltage"`
BatteryCurrent Current `json:"battery_current"`
BatteryMode int `json:"battery_mode"`
WarningCode int `json:"warning_code"`
SafetyCountryCode int `json:"safety_country_code"`
WorkMode int `json:"work_mode"`
OperationCode int `json:"operation_code"`
ErrorCodes int `json:"-"`
EnergyGenerationTotal Energy `json:"energy_generation_total"`
EnergyGenerationToday Energy `json:"energy_generation_today"`
EnergyExportTotal Energy `json:"energy_export_total"`
EnergyExportTotalHours int `json:"energy_export_total_hours"`
EnergyExportToday Energy `json:"energy_export_today"`
EnergyImportTotal Energy `json:"energy_import_total"`
EnergyImportToday Energy `json:"energy_import_today"`
EnergyLoadTotal Energy `json:"energy_load_total"`
EnergyLoadDay Energy `json:"energy_load_day"`
BatteryChargeTotal int `json:"battery_charge_total"`
BatteryChargeToday int `json:"battery_charge_today"`
BatteryDischargeTotal int `json:"battery_discharge_total"`
BatteryDischargeToday int `json:"battery_discharge_today"`
DiagStatusCode int `json:"-"`
HouseConsumption Power `json:"house_consumption"`
}