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..9a85078 100644 --- a/metrics.go +++ b/metrics.go @@ -84,6 +84,62 @@ var ( Name: "site_mppt_current_dc", Help: "Site mppt current DC in A", }, []string{"inverter", "mppt"}) + + siteRealtimeDataDcCurrentMPPT1Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_current_mppt1", + Help: "Site real time data DC current MPPT 1 in A", + }) + siteRealtimeDataDcCurrentMPPT2Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_current_mppt2", + Help: "Site real time data DC current MPPT 2 in A", + }) + siteRealtimeDataDcCurrentMPPT3Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_current_mppt3", + Help: "Site real time data DC current MPPT 3 in A", + }) + siteRealtimeDataDcCurrentMPPT4Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_current_mppt4", + Help: "Site real time data DC current MPPT 4 in A", + }) + siteRealtimeDataDcVoltageMPPT1Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_voltage_mppt1", + Help: "Site real time data DC voltage MPPT 1 in V", + }) + siteRealtimeDataDcVoltageMPPT2Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_voltage_mppt2", + Help: "Site real time data DC voltage MPPT 2 in V", + }) + siteRealtimeDataDcVoltageMPPT3Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_voltage_mppt3", + Help: "Site real time data DC voltage MPPT 3 in V", + }) + siteRealtimeDataDcVoltageMPPT4Gauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_dc_voltage_mppt4", + Help: "Site real time data DC voltage MPPT 4 in V", + }) + siteRealtimeDataAcFrequencyGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_ac_frequency", + Help: "Site real time data AC frequency in Hz", + }) + siteRealtimeDataAcPowerGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_ac_power", + Help: "Site real time data AC power in W", + }) + siteRealtimeDataTotalEnergyGeneratedGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "site_realtime_data_total_energy_generated", + Help: "Site real time data total energy generated in Wh", + }) ) func collectMetricsFromTarget(client *fronius.SymoClient) { @@ -96,10 +152,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 +176,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 +225,23 @@ func parsePowerFlowMetrics(data *fronius.SymoData) { } } +func parseInverterRealtimeData(data *fronius.SymoInverterRealtimeData) { + log.WithField("InverterRealtimeData", *data).Debug("Parsing data.") + siteRealtimeDataDcCurrentMPPT1Gauge.Set(data.DcCurrentMPPT1.Value) + siteRealtimeDataDcCurrentMPPT2Gauge.Set(data.DcCurrentMPPT2.Value) + siteRealtimeDataDcCurrentMPPT3Gauge.Set(data.DcCurrentMPPT3.Value) + siteRealtimeDataDcCurrentMPPT4Gauge.Set(data.DcCurrentMPPT4.Value) + + siteRealtimeDataDcVoltageMPPT1Gauge.Set(data.DcVoltageMPPT1.Value) + siteRealtimeDataDcVoltageMPPT2Gauge.Set(data.DcVoltageMPPT2.Value) + siteRealtimeDataDcVoltageMPPT3Gauge.Set(data.DcVoltageMPPT3.Value) + siteRealtimeDataDcVoltageMPPT4Gauge.Set(data.DcVoltageMPPT4.Value) + + siteRealtimeDataAcFrequencyGauge.Set(data.AcFrequency.Value) + siteRealtimeDataAcPowerGauge.Set(data.AcPower.Value) + siteRealtimeDataTotalEnergyGeneratedGauge.Set(data.TotalEnergyGenerated.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..1f70da4 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,39 @@ type ( EnergyTotal float64 `json:"E_Total"` } + symoInverterRealtime struct { + Body struct { + Data SymoInverterRealtimeData `json:"Data"` + } + } + SymoInverterRealtimeData struct { + //DC currents of MPPT (Maximum Power Point Tracking) 1 to 4 in ampere + DcCurrentMPPT1 RealTimeDataPoint `json:"IDC"` + DcCurrentMPPT2 RealTimeDataPoint `json:"IDC_2"` + DcCurrentMPPT3 RealTimeDataPoint `json:"IDC_3"` + DcCurrentMPPT4 RealTimeDataPoint `json:"IDC_4"` + + //DC voltages of MPPT (Maximum Power Point Tracking) 1 to 4 in Volt + DcVoltageMPPT1 RealTimeDataPoint `json:"UDC"` + DcVoltageMPPT2 RealTimeDataPoint `json:"UDC_2"` + DcVoltageMPPT3 RealTimeDataPoint `json:"UDC_3"` + DcVoltageMPPT4 RealTimeDataPoint `json:"UDC_4"` + + //AC frequency in Hz + AcFrequency RealTimeDataPoint `json:"FAC"` + + //AC power in Watt (negative value for consuming power) + AcPower RealTimeDataPoint `json:"PAC"` + + //AC Energy generated overall in Wh + TotalEnergyGenerated RealTimeDataPoint `json:"TOTAL_ENERGY"` + } + + 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 +128,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 +170,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..04482f4 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) @@ -58,3 +59,51 @@ func Test_Symo_GetArchiveData_GivenUrl_WhenRequestData_ThenParseStruct(t *testin assert.Equal(t, float64(425.6), p["inverter/1"].Data.VoltageDCString1.Values["0"]) assert.Equal(t, float64(408.90000000000003), p["inverter/1"].Data.VoltageDCString2.Values["0"]) } + +func Test_Symo_GetInverterRealtimeData_GivenUrl_WhenRequestData_ThenParseStruct(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + payload, err := os.ReadFile("testdata/realtimedata.json") + require.NoError(t, err) + _, _ = rw.Write(payload) + })) + + c, err := NewSymoClient(ClientOptions{ + URL: server.URL, + PowerFlowEnabled: true, + ArchiveEnabled: true, + InverterRealtimeEnabled: true, + }) + require.NoError(t, err) + + p, err := c.GetInverterRealtimeData() + assert.NoError(t, err) + + //current + assert.Equal(t, float64(0.021116470918059349), p.DcCurrentMPPT1.Value) + assert.Equal(t, float64(0.01560344360768795), p.DcCurrentMPPT2.Value) + assert.Equal(t, float64(0), p.DcCurrentMPPT3.Value) + assert.Equal(t, float64(0), p.DcCurrentMPPT4.Value) + assert.Equal(t, "A", p.DcCurrentMPPT1.Unit) + assert.Equal(t, "A", p.DcCurrentMPPT2.Unit) + assert.Equal(t, "A", p.DcCurrentMPPT3.Unit) + assert.Equal(t, "A", p.DcCurrentMPPT4.Unit) + + //voltage + assert.Equal(t, float64(44.587142944335938), p.DcVoltageMPPT1.Value) + assert.Equal(t, float64(72.194984436035156), p.DcVoltageMPPT2.Value) + assert.Equal(t, float64(0), p.DcVoltageMPPT3.Value) + assert.Equal(t, float64(0), p.DcVoltageMPPT4.Value) + assert.Equal(t, "V", p.DcVoltageMPPT1.Unit) + assert.Equal(t, "V", p.DcVoltageMPPT2.Unit) + assert.Equal(t, "V", p.DcVoltageMPPT3.Unit) + assert.Equal(t, "V", p.DcVoltageMPPT4.Unit) + + //AC frequency + assert.Equal(t, float64(50.029872894287109), p.AcFrequency.Value) + + //AC power + assert.Equal(t, float64(253.71487426757812), p.AcPower.Value) + + //Total energy generated + assert.Equal(t, float64(1392623.8052777778), p.TotalEnergyGenerated.Value) +} diff --git a/pkg/fronius/testdata/realtimedata.json b/pkg/fronius/testdata/realtimedata.json new file mode 100644 index 0000000..5aad1ef --- /dev/null +++ b/pkg/fronius/testdata/realtimedata.json @@ -0,0 +1,88 @@ +{ + "Body" : { + "Data" : { + "DAY_ENERGY" : { + "Unit" : "Wh", + "Value" : null + }, + "DeviceStatus" : { + "ErrorCode" : 0, + "InverterState" : "Running", + "StatusCode" : 7 + }, + "FAC" : { + "Unit" : "Hz", + "Value" : 50.029872894287109 + }, + "IAC" : { + "Unit" : "A", + "Value" : 1.0811097323894501 + }, + "IDC" : { + "Unit" : "A", + "Value" : 0.021116470918059349 + }, + "IDC_2" : { + "Unit" : "A", + "Value" : 0.01560344360768795 + }, + "IDC_3" : { + "Unit" : "A", + "Value" : null + }, + "IDC_4" : { + "Unit" : "A", + "Value" : null + }, + "PAC" : { + "Unit" : "W", + "Value" : 253.71487426757812 + }, + "SAC" : { + "Unit" : "VA", + "Value" : 253.71539306640625 + }, + "TOTAL_ENERGY" : { + "Unit" : "Wh", + "Value" : 1392623.8052777778 + }, + "UAC" : { + "Unit" : "V", + "Value" : 232.35920715332031 + }, + "UDC" : { + "Unit" : "V", + "Value" : 44.587142944335938 + }, + "UDC_2" : { + "Unit" : "V", + "Value" : 72.194984436035156 + }, + "UDC_3" : { + "Unit" : "V", + "Value" : null + }, + "UDC_4" : { + "Unit" : "V", + "Value" : null + }, + "YEAR_ENERGY" : { + "Unit" : "Wh", + "Value" : null + } + } + }, + "Head" : { + "RequestArguments" : { + "DataCollection" : "CommonInverterData", + "DeviceId" : "1", + "Scope" : "Device" + }, + "Status" : { + "Code" : 0, + "Reason" : "", + "UserMessage" : "" + }, + "Timestamp" : "2024-09-05T18:37:52+00:00" + } +}