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 scraping values from Fronius Smart Meter API #101

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions cfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ 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-smart-meter", config.Symo.SmartMeterEnabled, "Enable/disable scraping of smart meter data")
fs.Float64("symo.offset-consumed", config.Symo.OffsetConsumed, "Offset for consumed")
fs.Float64("symo.offset-produced", config.Symo.OffsetProduced, "Offset for produced")
Comment on lines +47 to +48
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please describe the flags better. What does "offset for consumed/produced" mean? What unit (if there is)?
If the values are only relevant for the smart meter endpoint, maybe make it symo.smartmeter.offset-{consumed/produced}?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offset is meant to make it possible to keep the metric in sync with the official electric meter. The unit is Wh.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks. I'd propose to update the desc accordingly (better description welcome).

Suggested change
fs.Float64("symo.offset-consumed", config.Symo.OffsetConsumed, "Offset for consumed")
fs.Float64("symo.offset-produced", config.Symo.OffsetProduced, "Offset for produced")
fs.Float64("symo.offset-consumed", config.Symo.OffsetConsumed, "Offset in Wh added to consumed energy to keep in sync with official meter. Only used if smart-meter endpoint is enabled.")
fs.Float64("symo.offset-produced", config.Symo.OffsetProduced, "Offset in Wh added to produced energy to keep in sync with official meter. Only used if smart-meter endpoint is enabled.")

}

func postLoadProcess(config *Configuration) {
Expand Down
26 changes: 16 additions & 10 deletions cfg/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ 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"`
SmartMeterEnabled bool `koanf:"enable-smart-meter"`
OffsetConsumed float64 `koanf:"offset-consumed"`
OffsetProduced float64 `koanf:"offset-produced"`
}
)

Expand All @@ -31,11 +34,14 @@ 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,
SmartMeterEnabled: false,
OffsetConsumed: 0.0,
OffsetProduced: 0.0,
},
BindAddr: ":8080",
}
Expand Down
15 changes: 9 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ 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,
SmartMeterEnabled: config.Symo.SmartMeterEnabled,
OffsetConsumed: config.Symo.OffsetConsumed,
OffsetProduced: config.Symo.OffsetProduced,
})
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.SmartMeterEnabled {
log.Fatal("All scrape endpoints are disabled. You need enable at least one endpoint.")
}

Expand Down
37 changes: 35 additions & 2 deletions metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ var (
sitePowerPhotovoltaicsGauge = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "site_power_photovoltaic",
Help: "Site power supplied to or provided from the accumulator(s) in Watt",
Help: "Site power from photovoltaic in Watt",
})

siteAutonomyRatioGauge = promauto.NewGauge(prometheus.GaugeOpts{
Expand Down Expand Up @@ -84,6 +84,16 @@ var (
Name: "site_mppt_current_dc",
Help: "Site mppt current DC in A",
}, []string{"inverter", "mppt"})
meterEnergyRealSumConsumedVec = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Name: "meter_energy_consumed_wh_total",
Help: "Meter consumed energy from grid in Wh",
}, []string{"device"})
meterEnergyRealSumProducedVec = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Name: "meter_energy_produced_wh_total",
Help: "Meter produced energy to grid in Wh",
}, []string{"device"})
)

func collectMetricsFromTarget(client *fronius.SymoClient) {
Expand All @@ -93,13 +103,15 @@ func collectMetricsFromTarget(client *fronius.SymoClient) {
"timeout": client.Options.Timeout,
"powerFlowEnabled": client.Options.PowerFlowEnabled,
"archiveEnabled": client.Options.ArchiveEnabled,
"meterEnabled": client.Options.SmartMeterEnabled,
}).Debug("Requesting data.")

wg := sync.WaitGroup{}
wg.Add(2)
wg.Add(3)

collectPowerFlowData(client, &wg)
collectArchiveData(client, &wg)
collectSmartMeterData(client, &wg)

wg.Wait()
elapsed := time.Since(start)
Expand Down Expand Up @@ -132,6 +144,19 @@ func collectArchiveData(client *fronius.SymoClient, w *sync.WaitGroup) {
}
}

func collectSmartMeterData(client *fronius.SymoClient, w *sync.WaitGroup) {
defer w.Done()
if client.Options.SmartMeterEnabled {
meterData, err := client.GetMeterData()
if err != nil {
log.WithError(err).Warn("Could not collect Symo SmartMeter metrics.")
scrapeErrorCount.Add(1)
return
}
parseSmartMeterMetrics(meterData)
}
}

func parsePowerFlowMetrics(data *fronius.SymoData) {
log.WithField("powerFlowData", *data).Debug("Parsing data.")
for key, inverter := range data.Inverters {
Expand Down Expand Up @@ -165,3 +190,11 @@ func parseArchiveMetrics(data map[string]fronius.InverterArchive) {
siteMPPTVoltageGaugeVec.WithLabelValues(key, "2").Set(inverter.Data.VoltageDCString2.Values["0"])
}
}

func parseSmartMeterMetrics(data map[string]fronius.MeterData) {
log.WithField("meterData", data).Debug("Parsing data.")
for key, meter := range data {
meterEnergyRealSumConsumedVec.WithLabelValues(key).Set(meter.EnergyRealSumConsumed + config.Symo.OffsetConsumed)
meterEnergyRealSumProducedVec.WithLabelValues(key).Set(meter.EnergyRealSumProduced + config.Symo.OffsetProduced)
Comment on lines +197 to +198
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the smart meter data...
Is this offset actually required or important here? What is the semantic of the offset? Or is this more relevant to your personal use case?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

}
}
62 changes: 53 additions & 9 deletions pkg/fronius/symo.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ 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"
// MeterDataPath is the Fronius API URL-path for Smart Meter real time data
MeterDataPath = "/solar_api/v1/GetMeterRealtimeData.cgi"
)

type (
symoPowerFlow struct {
SymoPowerFlow struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why expose this type? It's not meant to be public (i.e. usable by other Go packages), but is merely a struct to parse the JSON

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SymoPowerFlow struct {
symoPowerFlow struct {

Body struct {
Data SymoData
}
Expand Down Expand Up @@ -64,7 +66,7 @@ type (
}

// SymoArchive holds the parsed archive data from Symo API
symoArchive struct {
SymoArchive struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
SymoArchive struct {
symoArchive struct {

Body struct {
Data map[string]InverterArchive
}
Expand All @@ -86,18 +88,34 @@ type (
Values map[string]float64
}

symoMeter struct {
Body struct {
Data map[string]MeterData
}
}

MeterData struct {
// "EnergyReal_WAC_Sum_Consumed": 560839.0,
// "EnergyReal_WAC_Sum_Produced": 94087.0,
EnergyRealSumConsumed float64 `json:"EnergyReal_WAC_Sum_Consumed"`
EnergyRealSumProduced float64 `json:"EnergyReal_WAC_Sum_Produced"`
}

// SymoClient is a wrapper for making API requests against a Fronius Symo device.
SymoClient struct {
request *http.Request
Options ClientOptions
}
// 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
SmartMeterEnabled bool
OffsetConsumed float64
OffsetProduced float64
Comment on lines +117 to +118
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From this naming alone it's not clear to me what it means or when it's relevant. Please either try to find a better name, or add field comments describing its effects.

Comment on lines +117 to +118
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OffsetConsumed float64
OffsetProduced float64
// OffsetConsumed in Wh is added to consumed energy to keep in sync with official meter (only used if smart meter endpoint enabled).
OffsetConsumed float64
// OffsetProduced in Wh is added to consumed energy to keep in sync with official meter (only used if smart meter endpoint enabled).
OffsetProduced float64

}
)

Expand Down Expand Up @@ -126,7 +144,7 @@ func (c *SymoClient) GetPowerFlowData() (*SymoData, error) {
return nil, err
}
defer response.Body.Close()
p := symoPowerFlow{}
p := SymoPowerFlow{}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
p := SymoPowerFlow{}
p := symoPowerFlow{}

err = json.NewDecoder(response.Body).Decode(&p)
if err != nil {
return nil, err
Expand Down Expand Up @@ -159,7 +177,33 @@ func (c *SymoClient) GetArchiveData() (map[string]InverterArchive, error) {
return nil, err
}
defer response.Body.Close()
p := symoArchive{}
p := SymoArchive{}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
p := SymoArchive{}
p := symoArchive{}

err = json.NewDecoder(response.Body).Decode(&p)
if err != nil {
return nil, err
}
return p.Body.Data, nil
}

func (c *SymoClient) GetMeterData() (map[string]MeterData, error) {
u, err := url.Parse(c.Options.URL + MeterDataPath)
if err != nil {
return nil, err
}

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 := SymoMeter{}
err = json.NewDecoder(response.Body).Decode(&p)
if err != nil {
return nil, err
Expand Down
20 changes: 20 additions & 0 deletions pkg/fronius/symo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,23 @@ 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_GetMeterData_GivenUrl_WhenRequestData_ThenParseStruct(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
payload, err := os.ReadFile("testdata/test_meter.json")
require.NoError(t, err)
_, _ = rw.Write(payload)
Comment on lines +62 to +66
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for providing a test + testfile 👍

}))

c, err := NewSymoClient(ClientOptions{
URL: server.URL,
PowerFlowEnabled: true,
ArchiveEnabled: true,
})
require.NoError(t, err)

p, err := c.GetMeterData()
assert.NoError(t, err)
assert.Equal(t, float64(560839), p["0"].EnergyRealSumConsumed)
assert.Equal(t, float64(94087), p["0"].EnergyRealSumProduced)
}
61 changes: 61 additions & 0 deletions pkg/fronius/testdata/test_meter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"Body": {
"Data": {
"0": {
"Current_AC_Phase_1": 0.52100000000000002,
"Current_AC_Phase_2": 0.52300000000000002,
"Current_AC_Phase_3": -0.56999999999999995,
"Current_AC_Sum": 0.47400000000000009,
"Details": {
"Manufacturer": "Fronius",
"Model": "Smart Meter TS 65A-3",
"Serial": "08154711"
},
"Enable": 1,
"EnergyReactive_VArAC_Sum_Consumed": 1924.0,
"EnergyReactive_VArAC_Sum_Produced": 399648.0,
"EnergyReal_WAC_Minus_Absolute": 94087.0,
"EnergyReal_WAC_Plus_Absolute": 560839.0,
"EnergyReal_WAC_Sum_Consumed": 560839.0,
"EnergyReal_WAC_Sum_Produced": 94087.0,
"Frequency_Phase_Average": 50.0,
"Meter_Location_Current": 0.0,
"PowerApparent_S_Phase_1": 109.3,
"PowerApparent_S_Phase_2": 88.0,
"PowerApparent_S_Phase_3": 126.8,
"PowerApparent_S_Sum": 324.10000000000002,
"PowerFactor_Phase_1": 0.12,
"PowerFactor_Phase_2": 0.86499999999999999,
"PowerFactor_Phase_3": -0.98299999999999998,
"PowerFactor_Sum": -0.19800000000000001,
"PowerReactive_Q_Phase_1": -108.5,
"PowerReactive_Q_Phase_2": -44.200000000000003,
"PowerReactive_Q_Phase_3": -23.100000000000001,
"PowerReactive_Q_Sum": -175.80000000000001,
"PowerReal_P_Phase_1": 13.1,
"PowerReal_P_Phase_2": 76.0,
"PowerReal_P_Phase_3": -124.7,
"PowerReal_P_Sum": -35.5,
"TimeStamp": 1677777249,
"Visible": 1,
"Voltage_AC_PhaseToPhase_12": 398.69999999999999,
"Voltage_AC_PhaseToPhase_23": 401.39999999999998,
"Voltage_AC_PhaseToPhase_31": 398.60000000000002,
"Voltage_AC_Phase_1": 228.59999999999999,
"Voltage_AC_Phase_2": 231.80000000000001,
"Voltage_AC_Phase_3": 231.69999999999999
}
}
},
"Head": {
"RequestArguments": {
"Scope": "System"
},
"Status": {
"Code": 0,
"Reason": "",
"UserMessage": ""
},
"Timestamp": "2023-03-02T17:14:10+00:00"
}
}