diff --git a/tariff/octopus.go b/tariff/octopus.go new file mode 100644 index 000000000..5c280b8ec --- /dev/null +++ b/tariff/octopus.go @@ -0,0 +1,103 @@ +package tariff + +import ( + "errors" + "github.com/evcc-io/evcc/tariff/octopus" + "sync" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" +) + +type Octopus struct { + mux sync.Mutex + log *util.Logger + uri string + region string + data api.Rates + updated time.Time +} + +var _ api.Tariff = (*Octopus)(nil) + +func init() { + registry.Add("octopusenergy", NewOctopusFromConfig) +} + +func NewOctopusFromConfig(other map[string]interface{}) (api.Tariff, error) { + var cc struct { + Region string + Tariff string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + if cc.Region == "" { + return nil, errors.New("missing region") + } + if cc.Tariff == "" { + return nil, errors.New("missing tariff code") + } + + t := &Octopus{ + log: util.NewLogger("octopus"), + uri: octopus.ConstructRatesAPI(cc.Tariff, cc.Region), + region: cc.Tariff, + } + + done := make(chan error) + go t.run(done) + err := <-done + + return t, err +} + +func (t *Octopus) run(done chan error) { + var once sync.Once + client := request.NewHelper(t.log) + + for ; true; <-time.NewTicker(time.Hour).C { + var res octopus.UnitRates + if err := client.GetJSON(t.uri, &res); err != nil { + once.Do(func() { done <- err }) + + t.log.ERROR.Println(err) + continue + } + + once.Do(func() { close(done) }) + + t.mux.Lock() + t.updated = time.Now() + + t.data = make(api.Rates, 0, len(res.Results)) + for _, r := range res.Results { + ar := api.Rate{ + Start: r.ValidityStart, + End: r.ValidityEnd, + // UnitRates are supplied inclusive of tax, though this could be flipped easily with a config flag. + Price: r.PriceInclusiveTax / 1e2, + } + t.data = append(t.data, ar) + } + + t.mux.Unlock() + } +} + +// Unit implements the api.Tariff interface +// Stubbed because supplier always works in GBP +func (t *Octopus) Unit() string { + return "GBP" +} + +// Rates implements the api.Tariff interface +func (t *Octopus) Rates() (api.Rates, error) { + t.mux.Lock() + defer t.mux.Unlock() + return append([]api.Rate{}, t.data...), outdatedError(t.updated, time.Hour) +} diff --git a/tariff/octopus/api.go b/tariff/octopus/api.go new file mode 100644 index 000000000..1f96453a9 --- /dev/null +++ b/tariff/octopus/api.go @@ -0,0 +1,35 @@ +package octopus + +import ( + "fmt" + "strings" + "time" +) + +// ProductURI defines the location of the tariff information page. Substitute %s with tariff name. +const ProductURI = "https://api.octopus.energy/v1/products/%s/" + +// RatesURI defines the location of the full tariff rates page, including speculation. +// Substitute first %s with tariff name, second with region code. +const RatesURI = ProductURI + "electricity-tariffs/E-1R-%s-%s/standard-unit-rates/" + +// ConstructRatesAPI returns a validly formatted, fully qualified URI to the unit rate information. +func ConstructRatesAPI(tariff string, region string) string { + t := strings.ToUpper(tariff) + r := strings.ToUpper(region) + return fmt.Sprintf(RatesURI, t, t, r) +} + +type UnitRates struct { + Count uint64 `json:"count"` + Next string `json:"next"` + Previous string `json:"previous"` + Results []Rate `json:"results"` +} + +type Rate struct { + ValidityStart time.Time `json:"valid_from"` + ValidityEnd time.Time `json:"valid_to"` + PriceInclusiveTax float64 `json:"value_inc_vat"` + PriceExclusiveTax float64 `json:"value_exc_vat"` +}