Skip to content

Commit

Permalink
Add ElectricityMaps Co2 forecast (#5454)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Dec 23, 2022
1 parent a9666b5 commit 1684a0d
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 34 deletions.
1 change: 1 addition & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ type tariffConfig struct {
Currency string
Grid typedConfig
FeedIn typedConfig
Planner typedConfig
}

type networkConfig struct {
Expand Down
8 changes: 6 additions & 2 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
}
Expand Down
13 changes: 6 additions & 7 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions evcc.dist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion tariff/awattar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 29 additions & 12 deletions tariff/config.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
111 changes: 111 additions & 0 deletions tariff/electricitymaps.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 6 additions & 2 deletions tariff/fixed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 13 additions & 9 deletions tariff/tariffs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
6 changes: 5 additions & 1 deletion tariff/tibber.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1684a0d

Please sign in to comment.