octopus_exporter/cmd/octopus_exporter/rates.go
Rasmus "Pez" Wejlgaard dbcf50eb13
Some checks failed
Release / release (push) Has been cancelled
feat: solar export metrics (#16)
2026-05-25 11:56:21 +01:00

112 lines
3.3 KiB
Go

package main
import "time"
type tariffRates struct {
ElectricityUnitRate float64
ElectricityStandingCharge float64
ElectricityProductCode string
ElectricityTariffCode string
ElectricityIsAgile bool
GasUnitRate float64
GasStandingCharge float64
SolarExportRate float64
SolarHasExport bool
}
func getRates(token string) (*tariffRates, error) {
result, err := doGraphQL(gqlRequest{
Query: `{ viewer { accounts { ... on AccountType { properties {
electricityMeterPoints {
direction
agreements { validFrom validTo tariff {
... on StandardTariff { unitRate standingCharge productCode tariffCode }
... on HalfHourlyTariff { standingCharge productCode tariffCode }
... on PrepayTariff { unitRate standingCharge productCode tariffCode }
} }
}
gasMeterPoints {
agreements { validFrom validTo tariff { unitRate standingCharge } }
}
} } } } }`,
}, token)
if err != nil {
return nil, err
}
rates := &tariffRates{}
data, _ := result["data"].(map[string]any)
viewer, _ := data["viewer"].(map[string]any)
accounts, _ := viewer["accounts"].([]any)
for _, a := range accounts {
props, _ := a.(map[string]any)["properties"].([]any)
for _, p := range props {
pm, ok := p.(map[string]any)
if !ok {
continue
}
for _, mp := range toSlice(pm["electricityMeterPoints"]) {
tariff := activeAgreementTariff(mp)
if tariff == nil {
continue
}
mpm, _ := mp.(map[string]any)
if dir, _ := mpm["direction"].(string); dir == "EXPORT" {
rates.SolarExportRate, _ = tariff["unitRate"].(float64)
rates.SolarHasExport = true
continue
}
rates.ElectricityUnitRate, _ = tariff["unitRate"].(float64)
rates.ElectricityStandingCharge, _ = tariff["standingCharge"].(float64)
rates.ElectricityProductCode, _ = tariff["productCode"].(string)
rates.ElectricityTariffCode, _ = tariff["tariffCode"].(string)
// HalfHourlyTariff has no unitRate field — detect Agile by absence.
if _, hasUnit := tariff["unitRate"]; !hasUnit {
rates.ElectricityIsAgile = true
}
}
for _, mp := range toSlice(pm["gasMeterPoints"]) {
if tariff := activeAgreementTariff(mp); tariff != nil {
rates.GasUnitRate, _ = tariff["unitRate"].(float64)
rates.GasStandingCharge, _ = tariff["standingCharge"].(float64)
}
}
}
}
return rates, nil
}
// activeAgreementTariff returns the tariff for the agreement in effect now:
// validFrom in the past (or absent) and validTo null or in the future. Fixed-term
// tariffs carry a future validTo (their contract end), so matching only validTo==null
// would miss them.
func activeAgreementTariff(meterPoint any) map[string]any {
mp, ok := meterPoint.(map[string]any)
if !ok {
return nil
}
now := time.Now()
for _, ag := range toSlice(mp["agreements"]) {
agm, ok := ag.(map[string]any)
if !ok {
continue
}
if from, ok := agm["validFrom"].(string); ok {
if t, err := time.Parse(time.RFC3339, from); err == nil && t.After(now) {
continue // not started yet
}
}
if to, ok := agm["validTo"].(string); ok {
if t, err := time.Parse(time.RFC3339, to); err == nil && !t.After(now) {
continue // already ended
}
}
tariff, _ := agm["tariff"].(map[string]any)
return tariff
}
return nil
}