Add data types and test coverage
This commit is contained in:
parent
e467102cfe
commit
a103e2c4d8
|
@ -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 ./...
|
|
@ -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
11
go.mod
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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=
|
169
inverter/et.go
169
inverter/et.go
|
@ -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 (data *etRuntimeData) toRuntimeData(singlePhase bool) *ETRuntimeData {
|
||||
filterSinglePhase := func(i int) int {
|
||||
func filterSinglePhase[T numeric](v T, singlePhase bool) T {
|
||||
if singlePhase {
|
||||
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{
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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"`
|
||||
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 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"`
|
||||
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 int `json:"total_inverter_power"`
|
||||
ActivePower int `json:"active_power"`
|
||||
TotalInverterPower Power `json:"total_inverter_power"`
|
||||
ActivePower Power `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"`
|
||||
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 int `json:"backup_l1_power"`
|
||||
BackupL2Voltage int `json:"backup_l2_voltage"`
|
||||
BackupL2Current int `json:"backup_l2_current"`
|
||||
BackupL2Frequency int `json:"backup_l2_frequency"`
|
||||
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 int `json:"backup_l2_power"`
|
||||
BackupL3Voltage int `json:"backup_l3_voltage"`
|
||||
BackupL3Current int `json:"backup_l3_current"`
|
||||
BackupL3Frequency int `json:"backup_l3_frequency"`
|
||||
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 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"`
|
||||
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 int `json:"temperature_air"`
|
||||
TemperatureModule int `json:"temperature_module"`
|
||||
Temperature int `json:"temperature"`
|
||||
TemperatureAir Temp `json:"temperature_air"`
|
||||
TemperatureModule Temp `json:"temperature_module"`
|
||||
Temperature Temp `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"`
|
||||
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:"-"`
|
||||
PVGenerationTotal int `json:"pv_generation_total"`
|
||||
PVGenerationToday int `json:"pv_generation_today"`
|
||||
EnergyExportTotal int `json:"energy_export_total"`
|
||||
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 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"`
|
||||
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 int32 `json:"house_consumption"`
|
||||
HouseConsumption Power `json:"house_consumption"`
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue