Implement query meter data
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Rob Watson 2022-07-18 23:29:14 +02:00
parent 8c80417a53
commit def17ff49c
4 changed files with 208 additions and 9 deletions

View File

@ -14,8 +14,11 @@ import (
func main() {
var inverterAddr string
var meterData bool
var err error
flag.StringVar(&inverterAddr, "inverter-addr", "", "IP+port of solar inverter")
flag.BoolVar(&meterData, "meter-data", false, "print meter data, not sensors")
flag.Parse()
if inverterAddr == "" {
@ -30,16 +33,29 @@ func main() {
defer conn.Close()
var inv inverter.ET
var result []byte
runtimeData, err := inv.RuntimeData(context.Background(), conn)
if err != nil {
log.Fatalf("error fetching runtime data: %s", err)
if meterData {
meterData, err := inv.MeterData(context.Background(), conn)
if err != nil {
log.Fatalf("error fetching meter data: %s", err)
}
result, err = json.Marshal(meterData)
if err != nil {
log.Fatalf("error encoding meter data: %s", err)
}
} else {
runtimeData, err := inv.RuntimeData(context.Background(), conn)
if err != nil {
log.Fatalf("error fetching runtime data: %s", err)
}
result, err = json.Marshal(runtimeData)
if err != nil {
log.Fatalf("error encoding runtime data: %s", err)
}
}
json, err := json.Marshal(runtimeData)
if err != nil {
log.Fatalf("error encoding runtime data: %s", err)
}
fmt.Fprint(os.Stdout, string(json))
fmt.Fprint(os.Stdout, string(result))
}

View File

@ -59,6 +59,77 @@ func (info *etDeviceInfo) toDeviceInfo() *DeviceInfo {
}
}
// Unexported struct used for parsing binary data only.
type etMeterData struct {
ComMode int16
RSSI int16
ManufactureCode int16
MeterTestStatus int16
MeterCommStatus int16
ActivePowerL1 int16
ActivePowerL2 int16
ActivePowerL3 int16
ActivePowerTotal int16
ReactivePowerTotal int16
MeterPowerFactor1 int16
MeterPowerFactor2 int16
MeterPowerFactor3 int16
MeterPowerFactor int16
MeterFrequency int16
EnergyExportTotal float32
EnergyImportTotal float32
MeterActivePower1 int32
MeterActivePower2 int32
MeterActivePower3 int32
MeterActivePowerTotal int32
MeterReactivePower1 int32
MeterReactivePower2 int32
MeterReactivePower3 int32
MeterReactivePowerTotal int32
MeterApparentPower1 int32
MeterApparentPower2 int32
MeterApparentPower3 int32
MeterApparentPowerTotal int32
MeterType int16
MeterSoftwareVersion int16
}
func (data *etMeterData) toMeterData(singlePhase bool) *ETMeterData {
return &ETMeterData{
ComMode: int(data.ComMode),
RSSI: int(data.RSSI),
ManufactureCode: int(data.ManufactureCode),
MeterTestStatus: int(data.MeterTestStatus),
MeterCommStatus: int(data.MeterCommStatus),
ActivePowerL1: newPower(data.ActivePowerL1),
ActivePowerL2: newPower(data.ActivePowerL2),
ActivePowerL3: newPower(data.ActivePowerL3),
ActivePowerTotal: newPower(data.ActivePowerTotal),
ReactivePowerTotal: int(data.ReactivePowerTotal),
MeterPowerFactor1: float64(data.MeterPowerFactor1) / 1000.0,
MeterPowerFactor2: float64(filterSinglePhase(data.MeterPowerFactor2, singlePhase)) / 1000.0,
MeterPowerFactor3: float64(filterSinglePhase(data.MeterPowerFactor3, singlePhase)) / 1000.0,
MeterPowerFactor: float64(data.MeterPowerFactor) / 1000.0,
MeterFrequency: newFrequency(data.MeterFrequency),
EnergyExportTotal: newPower(data.EnergyExportTotal),
EnergyImportTotal: newPower(data.EnergyImportTotal),
MeterActivePower1: newPower(data.MeterActivePower1),
MeterActivePower2: newPower(data.MeterActivePower2),
MeterActivePower3: newPower(data.MeterActivePower3),
MeterActivePowerTotal: newPower(data.MeterActivePowerTotal),
MeterReactivePower1: int(data.MeterReactivePower1),
MeterReactivePower2: int(data.MeterReactivePower2),
MeterReactivePower3: int(data.MeterReactivePower3),
MeterReactivePowerTotal: int(data.MeterReactivePowerTotal),
MeterApparentPower1: int(data.MeterApparentPower1),
MeterApparentPower2: int(data.MeterApparentPower2),
MeterApparentPower3: int(data.MeterApparentPower3),
MeterApparentPowerTotal: int(data.MeterApparentPowerTotal),
MeterType: int(data.MeterType),
MeterSoftwareVersion: int(data.MeterSoftwareVersion),
}
}
// Unexported struct used for parsing binary data only.
//
// Raw types are based partly on the the PyPI library, and partly on the
@ -264,6 +335,15 @@ func (inv ET) DecodeRuntimeData(p []byte) (*ETRuntimeData, error) {
return runtimeData.toRuntimeData(inv.isSinglePhase()), nil
}
func (inv ET) DecodeMeterData(p []byte) (*ETMeterData, error) {
var meterData etMeterData
if err := binary.Read(bytes.NewReader(p), binary.BigEndian, &meterData); err != nil {
return nil, fmt.Errorf("error parsing response: %s", err)
}
return meterData.toMeterData(inv.isSinglePhase()), nil
}
// DEPRECATED
func (inv ET) DeviceInfo(ctx context.Context, conn command.Conn) (*DeviceInfo, error) {
resp, err := command.Send(command.NewModbus(command.ModbusCommandTypeRead, 0x88b8, 0x0021), conn)
@ -298,3 +378,19 @@ func (inv ET) RuntimeData(ctx context.Context, conn command.Conn) (*ETRuntimeDat
return runtimeData.toRuntimeData(deviceInfo.SinglePhase), nil
}
// DEPRECATED
func (inv ET) MeterData(ctx context.Context, conn command.Conn) (*ETMeterData, error) {
resp, err := command.Send(command.NewModbus(command.ModbusCommandTypeRead, 0x8ca0, 0x2d), conn)
if err != nil {
return nil, fmt.Errorf("error sending command: %s", err)
}
var meterData etMeterData
if err := binary.Read(bytes.NewReader(resp), binary.BigEndian, &meterData); err != nil {
return nil, fmt.Errorf("error parsing response: %s", err)
}
// TODO: wire in single phase:
return meterData.toMeterData(true), nil
}

View File

@ -139,3 +139,56 @@ func TestDecodeDeviceInfo(t *testing.T) {
assert.Equal(t, inverter.Power(0), runtimeData.LoadL3)
})
}
func TestDecodeMeterData(t *testing.T) {
inBytes := []byte{0, 1, 0, 45, 0, 10, 0, 0, 0, 1, 4, 114, 0, 0, 0, 0, 4, 114, 0, 226, 3, 201, 3, 231, 3, 231, 3, 200, 19, 132, 73, 48, 193, 246, 71, 195, 119, 16, 0, 0, 4, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 114, 0, 0, 0, 226, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 0, 0, 4, 151, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 151, 0, 255, 9, 44}
t.Run("with single-phase inverter", func(t *testing.T) {
inv := inverter.ET{SerialNumber: "foo"}
meterData, err := inv.DecodeMeterData(inBytes)
require.NoError(t, err)
assert.Equal(t, 1, meterData.ComMode)
assert.Equal(t, 45, meterData.RSSI)
assert.Equal(t, 10, meterData.ManufactureCode)
assert.Equal(t, 0, meterData.MeterTestStatus)
assert.Equal(t, 1, meterData.MeterCommStatus)
assert.Equal(t, inverter.Power(1138), meterData.ActivePowerL1)
assert.Equal(t, inverter.Power(0), meterData.ActivePowerL2)
assert.Equal(t, inverter.Power(0), meterData.ActivePowerL3)
assert.Equal(t, inverter.Power(1138), meterData.ActivePowerTotal)
assert.Equal(t, 226, meterData.ReactivePowerTotal)
assert.Equal(t, 0.969, meterData.MeterPowerFactor1)
assert.Equal(t, 0.999, meterData.MeterPowerFactor2)
assert.Equal(t, 0.999, meterData.MeterPowerFactor3)
assert.Equal(t, 0.968, meterData.MeterPowerFactor)
assert.Equal(t, inverter.Frequency(49.96), meterData.MeterFrequency)
assert.Equal(t, inverter.Power(723999.375000), meterData.EnergyExportTotal)
assert.Equal(t, inverter.Power(100078.125000), meterData.EnergyImportTotal)
assert.Equal(t, inverter.Power(1138), meterData.MeterActivePower1)
assert.Equal(t, inverter.Power(0), meterData.MeterActivePower2)
assert.Equal(t, inverter.Power(0), meterData.MeterActivePower3)
assert.Equal(t, inverter.Power(1138), meterData.MeterActivePowerTotal)
assert.Equal(t, 226, meterData.MeterReactivePower1)
assert.Equal(t, 0, meterData.MeterReactivePower2)
assert.Equal(t, 0, meterData.MeterReactivePower3)
assert.Equal(t, 226, meterData.MeterReactivePowerTotal)
assert.Equal(t, 1175, meterData.MeterApparentPower1)
assert.Equal(t, 0, meterData.MeterApparentPower2)
assert.Equal(t, 0, meterData.MeterApparentPower3)
assert.Equal(t, 1175, meterData.MeterApparentPowerTotal)
assert.Equal(t, 255, meterData.MeterType)
assert.Equal(t, 2348, meterData.MeterSoftwareVersion)
})
t.Run("with multi-phase inverter", func(t *testing.T) {
inv := inverter.ET{SerialNumber: "EHUfoo"}
meterData, err := inv.DecodeMeterData(inBytes)
require.NoError(t, err)
assert.Equal(t, 0.969, meterData.MeterPowerFactor1)
assert.Equal(t, 0.0, meterData.MeterPowerFactor2)
assert.Equal(t, 0.0, meterData.MeterPowerFactor3)
assert.Equal(t, 0.968, meterData.MeterPowerFactor)
})
}

View File

@ -136,3 +136,37 @@ type ETRuntimeData struct {
DiagStatusCode int `json:"-" db:"-"`
HouseConsumption Power `json:"house_consumption" db:"house_consumption"`
}
type ETMeterData struct {
ComMode int `json:"com_mode"`
RSSI int `json:"rssi"`
ManufactureCode int `json:"manufacture_code"`
MeterTestStatus int `json:"meter_test_status"`
MeterCommStatus int `json:"meter_comm_status"`
ActivePowerL1 Power `json:"active_power_l1"`
ActivePowerL2 Power `json:"active_power_l2"`
ActivePowerL3 Power `json:"active_power_l3"`
ActivePowerTotal Power `json:"active_power_total"`
ReactivePowerTotal int `json:"reactive_power_total"`
MeterPowerFactor1 float64 `json:"meter_power_factor1"`
MeterPowerFactor2 float64 `json:"meter_power_factor2"`
MeterPowerFactor3 float64 `json:"meter_power_factor3"`
MeterPowerFactor float64 `json:"meter_power_factor"`
MeterFrequency Frequency `json:"meter_frequency"`
EnergyExportTotal Power `json:"energy_export_total"`
EnergyImportTotal Power `json:"energy_import_total"`
MeterActivePower1 Power `json:"meter_active_power1"`
MeterActivePower2 Power `json:"meter_active_power2"`
MeterActivePower3 Power `json:"meter_active_power3"`
MeterActivePowerTotal Power `json:"meter_active_power_total"`
MeterReactivePower1 int `json:"meter_reactive_power1"`
MeterReactivePower2 int `json:"meter_reactive_power2"`
MeterReactivePower3 int `json:"meter_reactive_power3"`
MeterReactivePowerTotal int `json:"meter_reactive_power_total"`
MeterApparentPower1 int `json:"meter_apparent_power1"`
MeterApparentPower2 int `json:"meter_apparent_power2"`
MeterApparentPower3 int `json:"meter_apparent_power3"`
MeterApparentPowerTotal int `json:"meter_apparent_power_total"`
MeterType int `json:"meter_type"`
MeterSoftwareVersion int `json:"meter_software_version"`
}