diff --git a/cmd/config.go b/cmd/config.go index 0459b0fb77..16d3f56993 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -113,6 +113,7 @@ type tariffConfig struct { Currency string Grid typedConfig FeedIn typedConfig + Planner typedConfig } type networkConfig struct { diff --git a/cmd/setup.go b/cmd/setup.go index a1d375a7fc..b62c1af785 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -227,7 +227,7 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) (chan push.Eve } func configureTariffs(conf tariffConfig) (tariff.Tariffs, error) { - var grid, feedin api.Tariff + var grid, feedin, planner api.Tariff var currencyCode currency.Unit = currency.EUR var err error @@ -243,11 +243,15 @@ func configureTariffs(conf tariffConfig) (tariff.Tariffs, error) { feedin, err = tariff.NewFromConfig(conf.FeedIn.Type, conf.FeedIn.Other) } + if err == nil && conf.Planner.Type != "" { + planner, err = tariff.NewFromConfig(conf.Planner.Type, conf.Planner.Other) + } + if err != nil { err = fmt.Errorf("failed configuring tariff: %w", err) } - tariffs := tariff.NewTariffs(currencyCode, grid, feedin) + tariffs := tariff.NewTariffs(currencyCode, grid, feedin, planner) return *tariffs, err } diff --git a/core/site.go b/core/site.go index 85a7d50401..e0d00b41fb 100644 --- a/core/site.go +++ b/core/site.go @@ -127,16 +127,15 @@ func NewSiteFromConfig( }) } + tariff := site.tariffs.Grid + if site.tariffs.Planner != nil { + tariff = site.tariffs.Planner + } + // give loadpoints access to vehicles and database for _, lp := range loadpoints { lp.coordinator = coordinator.NewAdapter(lp, site.coordinator) - - // planner - gridTariff := site.tariffs.Grid - if gridTariff == nil { - gridTariff = new(tariff.Fixed) - } - lp.planner = planner.New(lp.log, gridTariff) + lp.planner = planner.New(lp.log, tariff) if serverdb.Instance != nil { var err error diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 24f40041f3..9061e68b1c 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -157,6 +157,10 @@ tariffs: # rate for feeding excess (pv) energy to the grid type: fixed price: 0.08 # EUR/kWh + planner: + # planner tariff can be used for target charging if not grid tariff is specified + # for example, electricitymaps provides CO2 intensity forecast + # type: electricitymaps # mqtt message broker mqtt: diff --git a/tariff/awattar.go b/tariff/awattar.go index 485663ca59..83a336142a 100644 --- a/tariff/awattar.go +++ b/tariff/awattar.go @@ -21,7 +21,11 @@ type Awattar struct { var _ api.Tariff = (*Awattar)(nil) -func NewAwattar(other map[string]interface{}) (*Awattar, error) { +func init() { + registry.Add("awattar", NewAwattarFromConfig) +} + +func NewAwattarFromConfig(other map[string]interface{}) (api.Tariff, error) { cc := struct { Cheap any // TODO deprecated Region string diff --git a/tariff/config.go b/tariff/config.go index 7ad3d833fe..fe04e43f39 100644 --- a/tariff/config.go +++ b/tariff/config.go @@ -1,23 +1,40 @@ package tariff import ( - "errors" + "fmt" "strings" "github.com/evcc-io/evcc/api" ) -// NewFromConfig creates new HEMS from config -func NewFromConfig(typ string, other map[string]interface{}) (t api.Tariff, err error) { - switch strings.ToLower(typ) { - case "fixed": - t, err = NewFixed(other) - case "awattar": - t, err = NewAwattar(other) - case "tibber": - t, err = NewTibber(other) - default: - return nil, errors.New("unknown tariff: " + typ) +type tariffRegistry map[string]func(map[string]interface{}) (api.Tariff, error) + +func (r tariffRegistry) Add(name string, factory func(map[string]interface{}) (api.Tariff, error)) { + if _, exists := r[name]; exists { + panic(fmt.Sprintf("cannot register duplicate tariff type: %s", name)) + } + r[name] = factory +} + +func (r tariffRegistry) Get(name string) (func(map[string]interface{}) (api.Tariff, error), error) { + factory, exists := r[name] + if !exists { + return nil, fmt.Errorf("tariff type not registered: %s", name) + } + return factory, nil +} + +var registry tariffRegistry = make(map[string]func(map[string]interface{}) (api.Tariff, error)) + +// NewFromConfig creates tariff from configuration +func NewFromConfig(typ string, other map[string]interface{}) (v api.Tariff, err error) { + factory, err := registry.Get(strings.ToLower(typ)) + if err == nil { + if v, err = factory(other); err != nil { + err = fmt.Errorf("cannot create tariff '%s': %w", typ, err) + } + } else { + err = fmt.Errorf("invalid tariff type: %s", typ) } return diff --git a/tariff/electricitymaps.go b/tariff/electricitymaps.go new file mode 100644 index 0000000000..b34b4797a5 --- /dev/null +++ b/tariff/electricitymaps.go @@ -0,0 +1,111 @@ +package tariff + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" +) + +type ElectricityMaps struct { + *request.Helper + log *util.Logger + mux sync.Mutex + uri string + zone string + data []CarbonIntensitySlot +} + +type CarbonIntensity struct { + Error string + Zone string + Forecast []CarbonIntensitySlot +} + +type CarbonIntensitySlot struct { + CarbonIntensity float64 // : 626, + Datetime time.Time // : "2022-12-12T16:00:00.000Z" +} + +var _ api.Tariff = (*ElectricityMaps)(nil) + +func init() { + registry.Add("electricitymaps", NewElectricityMapsFromConfig) +} + +func NewElectricityMapsFromConfig(other map[string]interface{}) (api.Tariff, error) { + cc := struct { + Uri string + Token string + Zone string + }{ + Zone: "DE", + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + log := util.NewLogger("em").Redact(cc.Token) + + t := &ElectricityMaps{ + log: log, + Helper: request.NewHelper(log), + uri: util.DefaultScheme(strings.TrimRight(cc.Uri, "/"), "https"), + zone: strings.ToUpper(cc.Zone), + } + + t.Client.Transport = &transport.Decorator{ + Base: t.Client.Transport, + Decorator: transport.DecorateHeaders(map[string]string{ + "X-BLOBR-KEY": cc.Token, + }), + } + + go t.Run() + + return t, nil +} + +func (t *ElectricityMaps) Run() { + uri := fmt.Sprintf("%s/carbon-intensity/forecast?zone=%s", t.uri, t.zone) + + for ; true; <-time.NewTicker(time.Hour).C { + var res CarbonIntensity + if err := t.GetJSON(uri, &res); err != nil { + if res.Error != "" { + err = errors.New(res.Error) + } + + t.log.ERROR.Println(err) + continue + } + + t.mux.Lock() + t.data = res.Forecast + t.mux.Unlock() + } +} + +func (t *ElectricityMaps) Rates() (api.Rates, error) { + t.mux.Lock() + defer t.mux.Unlock() + + res := make(api.Rates, 0, len(t.data)) + for _, r := range t.data { + ar := api.Rate{ + Start: r.Datetime, + End: r.Datetime.Add(time.Hour), + Price: r.CarbonIntensity, + } + res = append(res, ar) + } + + return res, nil +} diff --git a/tariff/fixed.go b/tariff/fixed.go index 78dfe318cf..d3306f14f8 100644 --- a/tariff/fixed.go +++ b/tariff/fixed.go @@ -13,8 +13,12 @@ type Fixed struct { var _ api.Tariff = (*Fixed)(nil) -func NewFixed(other map[string]interface{}) (*Fixed, error) { - cc := Fixed{} +func init() { + registry.Add("fixed", NewFixedFromConfig) +} + +func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) { + var cc Fixed if err := util.DecodeOther(other, &cc); err != nil { return nil, err diff --git a/tariff/tariffs.go b/tariff/tariffs.go index 0bcb72f485..08aef07374 100644 --- a/tariff/tariffs.go +++ b/tariff/tariffs.go @@ -6,15 +6,19 @@ import ( ) type Tariffs struct { - Currency currency.Unit - Grid api.Tariff - FeedIn api.Tariff + Currency currency.Unit + Grid, FeedIn, Planner api.Tariff } -func NewTariffs(currency currency.Unit, grid api.Tariff, feedin api.Tariff) *Tariffs { - t := Tariffs{} - t.Currency = currency - t.Grid = grid - t.FeedIn = feedin - return &t +func NewTariffs(currency currency.Unit, grid, feedin, planner api.Tariff) *Tariffs { + if planner == nil { + planner = grid + } + + return &Tariffs{ + Currency: currency, + Grid: grid, + FeedIn: feedin, + Planner: planner, + } } diff --git a/tariff/tibber.go b/tariff/tibber.go index f80634bd6c..684a04d9cd 100644 --- a/tariff/tibber.go +++ b/tariff/tibber.go @@ -23,7 +23,11 @@ type Tibber struct { var _ api.Tariff = (*Tibber)(nil) -func NewTibber(other map[string]interface{}) (*Tibber, error) { +func init() { + registry.Add("tibber", NewTibberFromConfig) +} + +func NewTibberFromConfig(other map[string]interface{}) (api.Tariff, error) { var cc struct { Token string HomeID string