From def17ff49c3bb57ee315fa489e5ea0c847d2bae0 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Mon, 18 Jul 2022 23:29:14 +0200 Subject: [PATCH] Implement query meter data --- cmd/status/main.go | 34 +++++++++++----- inverter/et.go | 96 +++++++++++++++++++++++++++++++++++++++++++++ inverter/et_test.go | 53 +++++++++++++++++++++++++ inverter/types.go | 34 ++++++++++++++++ 4 files changed, 208 insertions(+), 9 deletions(-) diff --git a/cmd/status/main.go b/cmd/status/main.go index 0e4aa05..44500cc 100644 --- a/cmd/status/main.go +++ b/cmd/status/main.go @@ -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)) } diff --git a/inverter/et.go b/inverter/et.go index d363c63..8e6bcd4 100644 --- a/inverter/et.go +++ b/inverter/et.go @@ -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 +} diff --git a/inverter/et_test.go b/inverter/et_test.go index 4f39dc0..25d871b 100644 --- a/inverter/et_test.go +++ b/inverter/et_test.go @@ -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) + }) +} diff --git a/inverter/types.go b/inverter/types.go index fbb58b9..ef79490 100644 --- a/inverter/types.go +++ b/inverter/types.go @@ -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"` +}