Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GetInverterRealtimeData endpoint #119

Merged
merged 4 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 12 additions & 10 deletions cfg/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
)

Expand All @@ -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",
}
Expand Down
13 changes: 7 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}

Expand Down
89 changes: 88 additions & 1 deletion metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 64 additions & 5 deletions pkg/fronius/symo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
)

Expand Down Expand Up @@ -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)
Expand Down
55 changes: 52 additions & 3 deletions pkg/fronius/symo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
}
Loading
Loading