diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index f5f9773..31012a4 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -31,7 +31,7 @@ func main() { } store := store.NewSQL(db) - handler := handler.NewHandler(store) + handler := handler.New(store) srv := http.Server{ ReadTimeout: time.Second * 3, WriteTimeout: time.Second * 3, diff --git a/gateway/handler/handler.go b/gateway/handler/handler.go index 122ed91..dd10f6a 100644 --- a/gateway/handler/handler.go +++ b/gateway/handler/handler.go @@ -1,11 +1,16 @@ package handler import ( + "encoding/json" + "io" + "log" "net/http" "git.netflux.io/rob/solar-toolkit/inverter" ) +const timestampMinimumYear = 2022 + type Store interface { InsertETRuntimeData(*inverter.ETRuntimeData) error } @@ -14,8 +19,47 @@ type Handler struct { store Store } -func NewHandler(store Store) *Handler { return &Handler{store: store} } +func New(store Store) *Handler { return &Handler{store: store} } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if r.URL.Path != "/gateway/et_runtime_data" { + http.Error(w, "endpoint not found", http.StatusNotFound) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + log.Printf("could not read body: %v", err) + http.Error(w, "unexpected error", http.StatusInternalServerError) + return + } + + var runtimeData inverter.ETRuntimeData + err = json.Unmarshal(body, &runtimeData) + if err != nil { + log.Printf("could not unmarshal body: %v", err) + http.Error(w, "unexpected error", http.StatusInternalServerError) + return + } + + if runtimeData.Timestamp.Year() < timestampMinimumYear { + log.Printf("invalid timestamp: %v", runtimeData.Timestamp) + http.Error(w, "invalid data", http.StatusBadRequest) + return + } + + if err = h.store.InsertETRuntimeData(&runtimeData); err != nil { + log.Printf("error storing data: %v", err) + http.Error(w, "unexpected error", http.StatusInternalServerError) + return + } + + w.Header().Set("content-type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK\n")) } diff --git a/gateway/handler/handler_test.go b/gateway/handler/handler_test.go new file mode 100644 index 0000000..71fc03e --- /dev/null +++ b/gateway/handler/handler_test.go @@ -0,0 +1,103 @@ +package handler_test + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.netflux.io/rob/solar-toolkit/gateway/handler" + "git.netflux.io/rob/solar-toolkit/inverter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type store struct { + err error +} + +func (s *store) InsertETRuntimeData(*inverter.ETRuntimeData) error { + return s.err +} + +func TestHandler(t *testing.T) { + testCases := []struct { + name string + httpMethod string + path string + body string + storeErr error + wantStatusCode int + wantBody string + }{ + { + name: "method not allowed", + httpMethod: http.MethodGet, + path: "/gateway/et_runtime_data", + wantStatusCode: http.StatusMethodNotAllowed, + wantBody: "method not allowed\n", + }, + { + name: "method not allowed", + httpMethod: http.MethodPost, + path: "/gateway/foo", + wantStatusCode: http.StatusNotFound, + wantBody: "endpoint not found\n", + }, + { + name: "invalid payload", + httpMethod: http.MethodPost, + path: "/gateway/et_runtime_data", + body: `{`, + wantStatusCode: http.StatusInternalServerError, + wantBody: "unexpected error\n", + }, + { + name: "invalid timestamp", + httpMethod: http.MethodPost, + path: "/gateway/et_runtime_data", + body: `{"timestamp": "1970-01-01T00:00:00Z"}`, + wantStatusCode: http.StatusBadRequest, + wantBody: "invalid data\n", + }, + { + name: "store error", + httpMethod: http.MethodPost, + path: "/gateway/et_runtime_data", + body: `{"timestamp": "2022-01-01T00:00:00Z"}`, + storeErr: errors.New("boom"), + wantStatusCode: http.StatusInternalServerError, + wantBody: "unexpected error\n", + }, + { + name: "OK", + httpMethod: http.MethodPost, + path: "/gateway/et_runtime_data", + body: `{"timestamp": "2022-01-01T00:00:00Z"}`, + wantStatusCode: http.StatusOK, + wantBody: "OK\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockStore := store{err: tc.storeErr} + handler := handler.New(&mockStore) + req := httptest.NewRequest(tc.httpMethod, tc.path, strings.NewReader(tc.body)) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + resp := rec.Result() + defer resp.Body.Close() + + assert.Equal(t, tc.wantStatusCode, resp.StatusCode) + + if tc.wantBody != "" { + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, tc.wantBody, string(body)) + } + }) + } +} diff --git a/gateway/sql/migrations/20220713135223_create_runtime_data_table.down.sql b/gateway/sql/migrations/20220713135223_create_runtime_data_table.down.sql new file mode 100644 index 0000000..1e9c079 --- /dev/null +++ b/gateway/sql/migrations/20220713135223_create_runtime_data_table.down.sql @@ -0,0 +1 @@ +DROP TABLE et_runtime_data; diff --git a/gateway/sql/migrations/20220713135223_create_runtime_data_table.up.sql b/gateway/sql/migrations/20220713135223_create_runtime_data_table.up.sql new file mode 100644 index 0000000..3ef6dec --- /dev/null +++ b/gateway/sql/migrations/20220713135223_create_runtime_data_table.up.sql @@ -0,0 +1,78 @@ +CREATE TABLE et_runtime_data ( + timestamp TIMESTAMP WITH TIME ZONE NOT NULL, + pv1_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv1_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv1_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv2_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv2_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv2_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + pv2_mode INT NOT NULL DEFAULT 0, + pv1_mode INT NOT NULL DEFAULT 0, + on_grid_l1_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l1_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l1_frequency DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l1_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l2_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l2_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l2_frequency DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l2_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l3_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l3_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l3_frequency DOUBLE PRECISION NOT NULL DEFAULT 0.0, + on_grid_l3_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + grid_mode INT NOT NULL DEFAULT 0, + total_inverter_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + active_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + reactive_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + apparent_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l1_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l1_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l1_frequency DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load_mode_l1 INT NOT NULL DEFAULT 0, + backup_l1_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l2_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l2_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l2_frequency DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load_mode_l2 INT NOT NULL DEFAULT 0, + backup_l2_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l3_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l3_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_l3_frequency DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load_mode_l3 INT NOT NULL DEFAULT 0, + backup_l3_power DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load_l1 DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load_l2 DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load_l3 DOUBLE PRECISION NOT NULL DEFAULT 0.0, + backup_load DOUBLE PRECISION NOT NULL DEFAULT 0.0, + load DOUBLE PRECISION NOT NULL DEFAULT 0.0, + ups_load DOUBLE PRECISION NOT NULL DEFAULT 0.0, + temperature_air DOUBLE PRECISION NOT NULL DEFAULT 0.0, + temperature_module DOUBLE PRECISION NOT NULL DEFAULT 0.0, + temperature DOUBLE PRECISION NOT NULL DEFAULT 0.0, + bus_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + nbus_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_voltage DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_current DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_mode INT NOT NULL DEFAULT 0, + warning_code INT NOT NULL DEFAULT 0, + safety_country_code INT NOT NULL DEFAULT 0, + work_mode INT NOT NULL DEFAULT 0, + operation_code INT NOT NULL DEFAULT 0, + energy_generation_total DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_generation_today DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_export_total DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_export_total_hours DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_export_today DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_import_total DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_import_today DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_load_total DOUBLE PRECISION NOT NULL DEFAULT 0.0, + energy_load_day DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_charge_total DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_charge_today DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_discharge_total DOUBLE PRECISION NOT NULL DEFAULT 0.0, + battery_discharge_today DOUBLE PRECISION NOT NULL DEFAULT 0.0, + house_consumption DOUBLE PRECISION NOT NULL DEFAULT 0.0 +); + +CREATE UNIQUE INDEX index_et_runtime_data_on_timestamp ON et_runtime_data (timestamp); diff --git a/gateway/store/store.go b/gateway/store/store.go index 4209d09..abc19eb 100644 --- a/gateway/store/store.go +++ b/gateway/store/store.go @@ -1,6 +1,8 @@ package store import ( + "fmt" + "git.netflux.io/rob/solar-toolkit/inverter" "github.com/jmoiron/sqlx" ) @@ -13,6 +15,12 @@ func NewSQL(db *sqlx.DB) *PostgresStore { return &PostgresStore{db: db} } +const insertSql = `INSERT INTO et_runtime_data (timestamp, pv1_voltage, pv1_current, pv1_power, pv2_voltage, pv2_current, pv2_power, pv_power, pv2_mode, pv1_mode, on_grid_l1_voltage, on_grid_l1_current, on_grid_l1_frequency, on_grid_l1_power, on_grid_l2_voltage, on_grid_l2_current, on_grid_l2_frequency, on_grid_l2_power, on_grid_l3_voltage, on_grid_l3_current, on_grid_l3_frequency, on_grid_l3_power, grid_mode, total_inverter_power, active_power, reactive_power, apparent_power, backup_l1_voltage, backup_l1_current, backup_l1_frequency, load_mode_l1, backup_l1_power, backup_l2_voltage, backup_l2_current, backup_l2_frequency, load_mode_l2, backup_l2_power, backup_l3_voltage, backup_l3_current, backup_l3_frequency, load_mode_l3, backup_l3_power, load_l1, load_l2, load_l3, backup_load, load, ups_load, temperature_air, temperature_module, temperature, bus_voltage, nbus_voltage, battery_voltage, battery_current, battery_mode, warning_code, safety_country_code, work_mode, operation_code, energy_generation_total, energy_generation_today, energy_export_total, energy_export_total_hours, energy_export_today, energy_import_total, energy_import_today, energy_load_total, energy_load_day, battery_charge_total, battery_charge_today, battery_discharge_total, battery_discharge_today, house_consumption) VALUES (:timestamp, :pv1_voltage, :pv1_current, :pv1_power, :pv2_voltage, :pv2_current, :pv2_power, :pv_power, :pv2_mode, :pv1_mode, :on_grid_l1_voltage, :on_grid_l1_current, :on_grid_l1_frequency, :on_grid_l1_power, :on_grid_l2_voltage, :on_grid_l2_current, :on_grid_l2_frequency, :on_grid_l2_power, :on_grid_l3_voltage, :on_grid_l3_current, :on_grid_l3_frequency, :on_grid_l3_power, :grid_mode, :total_inverter_power, :active_power, :reactive_power, :apparent_power, :backup_l1_voltage, :backup_l1_current, :backup_l1_frequency, :load_mode_l1, :backup_l1_power, :backup_l2_voltage, :backup_l2_current, :backup_l2_frequency, :load_mode_l2, :backup_l2_power, :backup_l3_voltage, :backup_l3_current, :backup_l3_frequency, :load_mode_l3, :backup_l3_power, :load_l1, :load_l2, :load_l3, :backup_load, :load, :ups_load, :temperature_air, :temperature_module, :temperature, :bus_voltage, :nbus_voltage, :battery_voltage, :battery_current, :battery_mode, :warning_code, :safety_country_code, :work_mode, :operation_code, :energy_generation_total, :energy_generation_today, :energy_export_total, :energy_export_total_hours, :energy_export_today, :energy_import_total, :energy_import_today, :energy_load_total, :energy_load_day, :battery_charge_total, :battery_charge_today, :battery_discharge_total, :battery_discharge_today, :house_consumption);` + func (s *PostgresStore) InsertETRuntimeData(runtimeData *inverter.ETRuntimeData) error { + if _, err := s.db.NamedExec(insertSql, runtimeData); err != nil { + return fmt.Errorf("error inserting data: %s", err) + } + return nil } diff --git a/inverter/types.go b/inverter/types.go index 761979c..fbb58b9 100644 --- a/inverter/types.go +++ b/inverter/types.go @@ -58,81 +58,81 @@ type DeviceInfo struct { } type ETRuntimeData struct { - 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"` + Timestamp time.Time `json:"timestamp" db:"timestamp"` + PV1Voltage Voltage `json:"pv1_voltage" db:"pv1_voltage"` + PV1Current Current `json:"pv1_current" db:"pv1_current"` + PV1Power Power `json:"pv1_power" db:"pv1_power"` + PV2Voltage Voltage `json:"pv2_voltage" db:"pv2_voltage"` + PV2Current Current `json:"pv2_current" db:"pv2_current"` + PV2Power Power `json:"pv2_power" db:"pv2_power"` + PVPower Power `json:"pv_power" db:"pv_power"` + PV2Mode byte `json:"pv2_mode" db:"pv2_mode"` + PV1Mode byte `json:"pv1_mode" db:"pv1_mode"` + OnGridL1Voltage Voltage `json:"on_grid_l1_voltage" db:"on_grid_l1_voltage"` + OnGridL1Current Current `json:"on_grid_l1_current" db:"on_grid_l1_current"` + OnGridL1Frequency Frequency `json:"on_grid_l1_frequency" db:"on_grid_l1_frequency"` + OnGridL1Power Power `json:"on_grid_l1_power" db:"on_grid_l1_power"` + OnGridL2Voltage Voltage `json:"on_grid_l2_voltage" db:"on_grid_l2_voltage"` + OnGridL2Current Current `json:"on_grid_l2_current" db:"on_grid_l2_current"` + OnGridL2Frequency Frequency `json:"on_grid_l2_frequency" db:"on_grid_l2_frequency"` + OnGridL2Power Power `json:"on_grid_l2_power" db:"on_grid_l2_power"` + OnGridL3Voltage Voltage `json:"on_grid_l3_voltage" db:"on_grid_l3_voltage"` + OnGridL3Current Current `json:"on_grid_l3_current" db:"on_grid_l3_current"` + OnGridL3Frequency Frequency `json:"on_grid_l3_frequency" db:"on_grid_l3_frequency"` + OnGridL3Power Power `json:"on_grid_l3_power" db:"on_grid_l3_power"` + GridMode int `json:"grid_mode" db:"grid_mode"` + TotalInverterPower Power `json:"total_inverter_power" db:"total_inverter_power"` + ActivePower Power `json:"active_power" db:"active_power"` + ReactivePower int `json:"reactive_power" db:"reactive_power"` + ApparentPower int `json:"apparent_power" db:"apparent_power"` + BackupL1Voltage Voltage `json:"backup_l1_voltage" db:"backup_l1_voltage"` + BackupL1Current Current `json:"backup_l1_current" db:"backup_l1_current"` + BackupL1Frequency Frequency `json:"backup_l1_frequency" db:"backup_l1_frequency"` + LoadModeL1 int `json:"load_mode_l1" db:"load_mode_l1"` + BackupL1Power Power `json:"backup_l1_power" db:"backup_l1_power"` + BackupL2Voltage Voltage `json:"backup_l2_voltage" db:"backup_l2_voltage"` + BackupL2Current Current `json:"backup_l2_current" db:"backup_l2_current"` + BackupL2Frequency Frequency `json:"backup_l2_frequency" db:"backup_l2_frequency"` + LoadModeL2 int `json:"load_mode_l2" db:"load_mode_l2"` + BackupL2Power Power `json:"backup_l2_power" db:"backup_l2_power"` + BackupL3Voltage Voltage `json:"backup_l3_voltage" db:"backup_l3_voltage"` + BackupL3Current Current `json:"backup_l3_current" db:"backup_l3_current"` + BackupL3Frequency Frequency `json:"backup_l3_frequency" db:"backup_l3_frequency"` + LoadModeL3 int `json:"load_mode_l3" db:"load_mode_l3"` + BackupL3Power Power `json:"backup_l3_power" db:"backup_l3_power"` + LoadL1 Power `json:"load_l1" db:"load_l1"` + LoadL2 Power `json:"load_l2" db:"load_l2"` + LoadL3 Power `json:"load_l3" db:"load_l3"` + BackupLoad Power `json:"backup_load" db:"backup_load"` + Load Power `json:"load" db:"load"` + UPSLoad int `json:"ups_load" db:"ups_load"` + TemperatureAir Temp `json:"temperature_air" db:"temperature_air"` + TemperatureModule Temp `json:"temperature_module" db:"temperature_module"` + Temperature Temp `json:"temperature" db:"temperature"` + FunctionBit int `json:"-" db:"-"` + BusVoltage Voltage `json:"bus_voltage" db:"bus_voltage"` + NBusVoltage Voltage `json:"nbus_voltage" db:"nbus_voltage"` + BatteryVoltage Voltage `json:"battery_voltage" db:"battery_voltage"` + BatteryCurrent Current `json:"battery_current" db:"battery_current"` + BatteryMode int `json:"battery_mode" db:"battery_mode"` + WarningCode int `json:"warning_code" db:"warning_code"` + SafetyCountryCode int `json:"safety_country_code" db:"safety_country_code"` + WorkMode int `json:"work_mode" db:"work_mode"` + OperationCode int `json:"operation_code" db:"operation_code"` + ErrorCodes int `json:"-" db:"-"` + EnergyGenerationTotal Energy `json:"energy_generation_total" db:"energy_generation_total"` + EnergyGenerationToday Energy `json:"energy_generation_today" db:"energy_generation_today"` + EnergyExportTotal Energy `json:"energy_export_total" db:"energy_export_total"` + EnergyExportTotalHours int `json:"energy_export_total_hours" db:"energy_export_total_hours"` + EnergyExportToday Energy `json:"energy_export_today" db:"energy_export_today"` + EnergyImportTotal Energy `json:"energy_import_total" db:"energy_import_total"` + EnergyImportToday Energy `json:"energy_import_today" db:"energy_import_today"` + EnergyLoadTotal Energy `json:"energy_load_total" db:"energy_load_total"` + EnergyLoadDay Energy `json:"energy_load_day" db:"energy_load_day"` + BatteryChargeTotal int `json:"battery_charge_total" db:"battery_charge_total"` + BatteryChargeToday int `json:"battery_charge_today" db:"battery_charge_today"` + BatteryDischargeTotal int `json:"battery_discharge_total" db:"battery_discharge_total"` + BatteryDischargeToday int `json:"battery_discharge_today" db:"battery_discharge_today"` + DiagStatusCode int `json:"-" db:"-"` + HouseConsumption Power `json:"house_consumption" db:"house_consumption"` }