From e7697e6466ce8aae8bf2171584553e1f40186ebb Mon Sep 17 00:00:00 2001 From: Christian Koch Date: Tue, 27 Aug 2024 19:56:52 +0200 Subject: [PATCH] add inverter realtime data with IDC and UDC data fields --- cfg/config.go | 1 + cfg/types.go | 22 +++++++------ main.go | 13 ++++---- metrics.go | 70 +++++++++++++++++++++++++++++++++++++++- pkg/fronius/symo.go | 58 ++++++++++++++++++++++++++++++--- pkg/fronius/symo_test.go | 7 ++-- 6 files changed, 146 insertions(+), 25 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index fd896e1..700e3d7 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -43,6 +43,7 @@ func setupCliFlags(version string, fs *flag.FlagSet, config *Configuration) { "Timeout in seconds when collecting metrics from Fronius Symo. Should not be larger than the scrape interval.") fs.Bool("symo.enable-power-flow", config.Symo.PowerFlowEnabled, "Enable/disable scraping of power flow data") fs.Bool("symo.enable-archive", config.Symo.ArchiveEnabled, "Enable/disable scraping of archive data") + fs.Bool("symo.enable-inverter-realtime", config.Symo.InverterRealtimeEnabled, "Enable/disable scraping of inverter real time data") } func postLoadProcess(config *Configuration) { diff --git a/cfg/types.go b/cfg/types.go index 6606a53..e60230d 100644 --- a/cfg/types.go +++ b/cfg/types.go @@ -16,11 +16,12 @@ type ( } // SymoConfig configures the Fronius Symo device SymoConfig struct { - URL string `koanf:"url"` - Timeout time.Duration `koanf:"timeout"` - Headers []string `koanf:"header"` - PowerFlowEnabled bool `koanf:"enable-power-flow"` - ArchiveEnabled bool `koanf:"enable-archive"` + URL string `koanf:"url"` + Timeout time.Duration `koanf:"timeout"` + Headers []string `koanf:"header"` + PowerFlowEnabled bool `koanf:"enable-power-flow"` + ArchiveEnabled bool `koanf:"enable-archive"` + InverterRealtimeEnabled bool `koanf:"enable-inverter-realtime"` } ) @@ -31,11 +32,12 @@ func NewDefaultConfig() *Configuration { Level: "info", }, Symo: SymoConfig{ - URL: "http://symo.ip.or.hostname", - Timeout: 5 * time.Second, - Headers: []string{}, - PowerFlowEnabled: true, - ArchiveEnabled: true, + URL: "http://symo.ip.or.hostname", + Timeout: 5 * time.Second, + Headers: []string{}, + PowerFlowEnabled: true, + ArchiveEnabled: true, + InverterRealtimeEnabled: true, }, BindAddr: ":8080", } diff --git a/main.go b/main.go index a45338c..482da3a 100644 --- a/main.go +++ b/main.go @@ -30,16 +30,17 @@ func main() { headers := http.Header{} cfg.ConvertHeaders(config.Symo.Headers, &headers) symoClient, err := fronius.NewSymoClient(fronius.ClientOptions{ - URL: config.Symo.URL, - Headers: headers, - Timeout: config.Symo.Timeout, - PowerFlowEnabled: config.Symo.PowerFlowEnabled, - ArchiveEnabled: config.Symo.ArchiveEnabled, + URL: config.Symo.URL, + Headers: headers, + Timeout: config.Symo.Timeout, + PowerFlowEnabled: config.Symo.PowerFlowEnabled, + ArchiveEnabled: config.Symo.ArchiveEnabled, + InverterRealtimeEnabled: config.Symo.InverterRealtimeEnabled, }) if err != nil { log.WithError(err).Fatal("Cannot initialize Fronius Symo client.") } - if !config.Symo.ArchiveEnabled && !config.Symo.PowerFlowEnabled { + if !config.Symo.ArchiveEnabled && !config.Symo.PowerFlowEnabled && !config.Symo.InverterRealtimeEnabled { log.Fatal("All scrape endpoints are disabled. You need enable at least one endpoint.") } diff --git a/metrics.go b/metrics.go index af73ab1..3ed80fd 100644 --- a/metrics.go +++ b/metrics.go @@ -84,6 +84,47 @@ var ( Name: "site_mppt_current_dc", Help: "Site mppt current DC in A", }, []string{"inverter", "mppt"}) + + siteRealtimeDataIDCGauge1 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_idc1", + Help: "Site real time data DC current string 1", + }) + siteRealtimeDataIDCGauge2 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_idc2", + Help: "Site real time data DC current string 2", + }) + siteRealtimeDataIDCGauge3 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_idc3", + Help: "Site real time data DC current string 3", + }) + siteRealtimeDataIDCGauge4 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_idc4", + Help: "Site real time data DC current string 4", + }) + siteRealtimeDataUDCGauge1 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_udc1", + Help: "Site real time data DC voltage string 1", + }) + siteRealtimeDataUDCGauge2 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_udc2", + Help: "Site real time data DC voltage string 2", + }) + siteRealtimeDataUDCGauge3 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_udc3", + Help: "Site real time data DC voltage string 3", + }) + siteRealtimeDataUDCGauge4 = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_udc4", + Help: "Site real time data DC voltage string 4", + }) ) func collectMetricsFromTarget(client *fronius.SymoClient) { @@ -96,10 +137,11 @@ func collectMetricsFromTarget(client *fronius.SymoClient) { }).Debug("Requesting data.") wg := sync.WaitGroup{} - wg.Add(2) + wg.Add(3) collectPowerFlowData(client, &wg) collectArchiveData(client, &wg) + collectInverterRealtimeData(client, &wg) wg.Wait() elapsed := time.Since(start) @@ -119,6 +161,19 @@ func collectPowerFlowData(client *fronius.SymoClient, w *sync.WaitGroup) { } } +func collectInverterRealtimeData(client *fronius.SymoClient, w *sync.WaitGroup) { + defer w.Done() + if client.Options.InverterRealtimeEnabled { + powerFlowData, err := client.GetInverterRealtimeData() + if err != nil { + log.WithError(err).Warn("Could not collect Symo inverter realtime metrics.") + scrapeErrorCount.Add(1) + return + } + parseInverterRealtimeData(powerFlowData) + } +} + func collectArchiveData(client *fronius.SymoClient, w *sync.WaitGroup) { defer w.Done() if client.Options.ArchiveEnabled { @@ -155,6 +210,19 @@ func parsePowerFlowMetrics(data *fronius.SymoData) { } } +func parseInverterRealtimeData(data *fronius.SymoInverterRealtimeData) { + log.WithField("InverterRealtimeData", *data).Debug("Parsing data.") + siteRealtimeDataIDCGauge1.Set(data.IDC1.Value) + siteRealtimeDataIDCGauge2.Set(data.IDC2.Value) + siteRealtimeDataIDCGauge3.Set(data.IDC3.Value) + siteRealtimeDataIDCGauge4.Set(data.IDC4.Value) + + siteRealtimeDataUDCGauge1.Set(data.UDC1.Value) + siteRealtimeDataUDCGauge2.Set(data.UDC2.Value) + siteRealtimeDataUDCGauge3.Set(data.UDC3.Value) + siteRealtimeDataUDCGauge4.Set(data.UDC4.Value) +} + func parseArchiveMetrics(data map[string]fronius.InverterArchive) { log.WithField("archiveData", data).Debug("Parsing data.") for key, inverter := range data { diff --git a/pkg/fronius/symo.go b/pkg/fronius/symo.go index 0152a0f..9d3abf0 100644 --- a/pkg/fronius/symo.go +++ b/pkg/fronius/symo.go @@ -13,6 +13,8 @@ const ( PowerDataPath = "/solar_api/v1/GetPowerFlowRealtimeData.fcgi" // ArchiveDataPath is the Fronius API URL-path for archive data ArchiveDataPath = "/solar_api/v1/GetArchiveData.cgi?Scope=System&Channel=Voltage_DC_String_1&Channel=Current_DC_String_1&Channel=Voltage_DC_String_2&Channel=Current_DC_String_2&HumanReadable=false" + // InverterRealtimeDataPath is the Fronius API URL-path for inverter real time data + InverterRealtimeDataPath = "/solar_api/v1/GetInverterRealtimeData.cgi?Scope=Device&DeviceId=1&DataCollection=CommonInverterData" ) type ( @@ -63,6 +65,28 @@ type ( EnergyTotal float64 `json:"E_Total"` } + symoInverterRealtime struct { + Body struct { + Data SymoInverterRealtimeData `json:"Data"` + } + } + SymoInverterRealtimeData struct { + IDC1 RealTimeDataPoint `json:"IDC"` + IDC2 RealTimeDataPoint `json:"IDC_2"` + IDC3 RealTimeDataPoint `json:"IDC_3"` + IDC4 RealTimeDataPoint `json:"IDC_4"` + + UDC1 RealTimeDataPoint `json:"UDC"` + UDC2 RealTimeDataPoint `json:"UDC_2"` + UDC3 RealTimeDataPoint `json:"UDC_3"` + UDC4 RealTimeDataPoint `json:"UDC_4"` + } + + RealTimeDataPoint struct { + Unit string `json:"Unit"` + Value float64 `json:"Value"` + } + // SymoArchive holds the parsed archive data from Symo API symoArchive struct { Body struct { @@ -93,11 +117,12 @@ type ( } // ClientOptions holds some parameters for the SymoClient. ClientOptions struct { - URL string - Headers http.Header - Timeout time.Duration - PowerFlowEnabled bool - ArchiveEnabled bool + URL string + Headers http.Header + Timeout time.Duration + PowerFlowEnabled bool + ArchiveEnabled bool + InverterRealtimeEnabled bool } ) @@ -134,6 +159,29 @@ func (c *SymoClient) GetPowerFlowData() (*SymoData, error) { return &p.Body.Data, nil } +// GetPowerFlowData returns the parsed data from the Symo device. +func (c *SymoClient) GetInverterRealtimeData() (*SymoInverterRealtimeData, error) { + u, err := url.Parse(c.Options.URL + InverterRealtimeDataPath) + if err != nil { + return nil, err + } + + c.request.URL = u + client := http.DefaultClient + client.Timeout = c.Options.Timeout + response, err := client.Do(c.request) + if err != nil { + return nil, err + } + defer response.Body.Close() + p := symoInverterRealtime{} + err = json.NewDecoder(response.Body).Decode(&p) + if err != nil { + return nil, err + } + return &p.Body.Data, nil +} + // GetArchiveData returns the parsed data from the Symo device. func (c *SymoClient) GetArchiveData() (map[string]InverterArchive, error) { u, err := url.Parse(c.Options.URL + ArchiveDataPath) diff --git a/pkg/fronius/symo_test.go b/pkg/fronius/symo_test.go index d4767d5..fb00557 100644 --- a/pkg/fronius/symo_test.go +++ b/pkg/fronius/symo_test.go @@ -45,9 +45,10 @@ func Test_Symo_GetArchiveData_GivenUrl_WhenRequestData_ThenParseStruct(t *testin })) c, err := NewSymoClient(ClientOptions{ - URL: server.URL, - PowerFlowEnabled: true, - ArchiveEnabled: true, + URL: server.URL, + PowerFlowEnabled: true, + ArchiveEnabled: true, + InverterRealtimeEnabled: true, }) require.NoError(t, err)