Add ElectricityMaps Co2 forecast (#5454)

This commit is contained in:
andig 2022-12-23 11:21:03 +01:00 • committed by GitHub
parent a9666b57ee
commit 1684a0d8e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 34 deletions

View file

@ -113,6 +113,7 @@ type tariffConfig struct {
Currency string
Grid typedConfig
FeedIn typedConfig
Planner typedConfig
}
type networkConfig struct {

View file

@ -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
}

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

111
tariff/electricitymaps.go Normal file
View file

@ -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
}

View file

@ -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

View file

@ -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,
}
}

View file

@ -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