Add support for Amber Electric (AU) (#12381)
This commit is contained in:
parent
5c5e7d9d12
commit
bcdec9671c
4 changed files with 179 additions and 0 deletions
|
|
@ -183,6 +183,11 @@ tariffs:
|
|||
# charges: # optional, additional charges per kWh
|
||||
# tax: # optional, additional tax (0.1 for 10%)
|
||||
|
||||
# type: amber
|
||||
# token: # api token from https://app.amber.com.au/developers/
|
||||
# siteid: # site ID returned by the API
|
||||
# channel: general
|
||||
|
||||
# type: custom # price from a plugin source; see https://docs.evcc.io/docs/reference/plugins
|
||||
# price:
|
||||
# source: http
|
||||
|
|
@ -197,6 +202,11 @@ tariffs:
|
|||
# type: octopusenergy
|
||||
# tariff: AGILE-FLEX-22-11-25 # Tariff code
|
||||
# region: A # optional
|
||||
|
||||
# type: amber
|
||||
# token: # api token from https://app.amber.com.au/developers/
|
||||
# siteid: # site ID returned by the API
|
||||
# channel: feedIn
|
||||
co2:
|
||||
# co2 tariff provides co2 intensity forecast and is for co2-optimized target charging if no variable grid tariff is specified
|
||||
# type: grünstromindex # GrünStromIndex (Germany only)
|
||||
|
|
|
|||
136
tariff/amber.go
Normal file
136
tariff/amber.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package tariff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/tariff/amber"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type Amber struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
log *util.Logger
|
||||
uri string
|
||||
channel string
|
||||
data *util.Monitor[api.Rates]
|
||||
}
|
||||
|
||||
var _ api.Tariff = (*Amber)(nil)
|
||||
|
||||
func init() {
|
||||
registry.Add("amber", NewAmberFromConfig)
|
||||
}
|
||||
|
||||
func NewAmberFromConfig(other map[string]interface{}) (api.Tariff, error) {
|
||||
var cc struct {
|
||||
embed `mapstructure:",squash"`
|
||||
Token string
|
||||
SiteID string
|
||||
Channel string
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.Token == "" {
|
||||
return nil, errors.New("missing token")
|
||||
}
|
||||
|
||||
if cc.SiteID == "" {
|
||||
return nil, errors.New("missing siteid")
|
||||
}
|
||||
|
||||
if cc.Channel == "" {
|
||||
return nil, errors.New("missing channel")
|
||||
}
|
||||
|
||||
log := util.NewLogger("amber").Redact(cc.Token)
|
||||
|
||||
t := &Amber{
|
||||
embed: &cc.embed,
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
uri: fmt.Sprintf(amber.URI, strings.ToUpper(cc.SiteID), time.Now().AddDate(0, 0, 1).Format("2006-01-02")),
|
||||
channel: strings.ToLower(cc.Channel),
|
||||
data: util.NewMonitor[api.Rates](2 * time.Hour),
|
||||
}
|
||||
|
||||
t.Client.Transport = &transport.Decorator{
|
||||
Base: t.Client.Transport,
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", cc.Token),
|
||||
}),
|
||||
}
|
||||
|
||||
done := make(chan error)
|
||||
go t.run(done)
|
||||
err := <-done
|
||||
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (t *Amber) run(done chan error) {
|
||||
var once sync.Once
|
||||
bo := newBackoff()
|
||||
|
||||
for ; true; <-time.Tick(time.Minute) {
|
||||
var res []amber.PriceInfo
|
||||
|
||||
if err := backoff.Retry(func() error {
|
||||
return t.GetJSON(t.uri, &res)
|
||||
}, bo); err != nil {
|
||||
once.Do(func() { done <- err })
|
||||
|
||||
t.log.ERROR.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
data := make(api.Rates, 0, len(res))
|
||||
|
||||
for _, r := range res {
|
||||
if t.channel == strings.ToLower(r.ChannelType) {
|
||||
startTime, _ := time.Parse("2006-01-02T15:04:05Z", r.StartTime)
|
||||
endTime, _ := time.Parse("2006-01-02T15:04:05Z", r.EndTime)
|
||||
ar := api.Rate{
|
||||
Start: startTime.Local(),
|
||||
End: endTime.Local(),
|
||||
Price: r.PerKwh / 1e2,
|
||||
}
|
||||
data = append(data, ar)
|
||||
}
|
||||
}
|
||||
data.Sort()
|
||||
|
||||
t.data.Set(data)
|
||||
once.Do(func() { close(done) })
|
||||
}
|
||||
}
|
||||
|
||||
// Rates implements the api.Tariff interface
|
||||
func (t *Amber) Rates() (api.Rates, error) {
|
||||
var res api.Rates
|
||||
err := t.data.GetFunc(func(val api.Rates) {
|
||||
res = slices.Clone(val)
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (t *Amber) Unit() string {
|
||||
return "AUD"
|
||||
}
|
||||
|
||||
// Type returns the tariff type
|
||||
func (t *Amber) Type() api.TariffType {
|
||||
return api.TariffTypePriceForecast
|
||||
}
|
||||
19
tariff/amber/types.go
Normal file
19
tariff/amber/types.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package amber
|
||||
|
||||
const URI = "https://api.amber.com.au/v1/sites/%s/prices?endDate=%s&resolution=30"
|
||||
|
||||
type PriceInfo struct {
|
||||
Type string `json:"type"`
|
||||
Date string `json:"date"`
|
||||
Duration int `json:"duration"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
NemTime string `json:"nemTime"`
|
||||
PerKwh float64 `json:"perKwh"`
|
||||
Renewables float64 `json:"renewables"`
|
||||
SpotPerKwh float64 `json:"spotPerKwh"`
|
||||
ChannelType string `json:"channelType"`
|
||||
SpikeStatus string `json:"spikeStatus"`
|
||||
Descriptor string `json:"descriptor"`
|
||||
Estimate bool `json:"estimate"`
|
||||
}
|
||||
14
templates/definition/tariff/amber.yaml
Normal file
14
templates/definition/tariff/amber.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
template: amber
|
||||
products:
|
||||
- brand: Amber Electric
|
||||
params:
|
||||
- preset: tariff-base
|
||||
- name: token
|
||||
- name: siteid
|
||||
- name: channel
|
||||
render: |
|
||||
type: amber
|
||||
{{ include "tariff-base" . }}
|
||||
token: {{ .token }}
|
||||
siteid: {{ .siteid }}
|
||||
channel: {{ .channel }}
|
||||
Loading…
Add table
Add a link
Reference in a new issue