Skip to content

Commit

Permalink
Add Awattar and Tibber (evcc-io#1169)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jul 24, 2021
1 parent d1ec6f6 commit 986d734
Show file tree
Hide file tree
Showing 17 changed files with 414 additions and 32 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
- [Meter](#meter)
- [Vehicle](#vehicle)
- [Home Energy Management System](#home-energy-management-system)
- [Flexible Energy Tariffs](#flexible-energy-tariffs)
- [Plugins](#plugins)
- [Modbus (read/write)](#modbus-readwrite)
- [MQTT (read/write)](#mqtt-readwrite)
Expand Down Expand Up @@ -295,6 +296,25 @@ to the configuration. The EVCC loadpoints can then be added to the SHM configura
Sunny-Portal via the "Optional energy demand" slider. When the amount of configured PV is not available, charging suspends like in **PV** mode. So, pushing the slider completely
to the left makes **Min+PV** behave as described above. Pushing completely to the right makes **Min+PV** mode behave like **PV** mode.
### Flexible Energy Tariffs
EVCC supports flexible energy tariffs as offered by [Awattar](https://www.awattar.de) or [Tibber](https://tibber.com). Configuration allows to define a "cheap" rate at which charging from grid is enabled at highest possible rate even when not enough PV power is locally available:
```yaml
tariffs:
grid:
# either
type: tibber
cheap: 20 # ct/kWh
token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4" # access token
homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c" # optional if multiple homes associated to account
# or
type: awattar
cheap: 20 # ct/kWh
region: de # optional, chose at for Austria
```
## Plugins
Plugins are used to integrate various devices and external data sources with EVCC. Plugins can be used in combination with a `custom` type meter, charger or vehicle.
Expand Down
4 changes: 4 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,7 @@ type VehicleStartCharge interface {
type VehicleStopCharge interface {
StopCharge() error
}

type Tariff interface {
IsCheap() bool
}
5 changes: 5 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type config struct {
Meters []qualifiedConfig
Chargers []qualifiedConfig
Vehicles []qualifiedConfig
Tariffs tariffConfig
Site map[string]interface{}
LoadPoints []map[string]interface{}
}
Expand Down Expand Up @@ -61,6 +62,10 @@ type messagingConfig struct {
Services []typedConfig
}

type tariffConfig struct {
Grid typedConfig
}

// ConfigProvider provides configuration items
type ConfigProvider struct {
meters map[string]api.Meter
Expand Down
25 changes: 22 additions & 3 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (
"strconv"
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/api/proto/pb"
"github.com/andig/evcc/core"
"github.com/andig/evcc/hems"
"github.com/andig/evcc/provider/javascript"
"github.com/andig/evcc/provider/mqtt"
"github.com/andig/evcc/push"
"github.com/andig/evcc/server"
"github.com/andig/evcc/tariff"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/cloud"
"github.com/andig/evcc/util/pipe"
Expand Down Expand Up @@ -165,21 +167,38 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Even
return notificationChan
}

func configureTariffs(conf tariffConfig) (t api.Tariff, err error) {
if conf.Grid.Type != "" {
t, err = tariff.NewFromConfig(conf.Grid.Type, conf.Grid.Other)
}

if err != nil {
err = fmt.Errorf("failed configuring tariff: %w", err)
}

return t, err
}

func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) {
if err = cp.configure(conf); err == nil {
var loadPoints []*core.LoadPoint
loadPoints, err = configureLoadPoints(conf, cp)

var tariff api.Tariff
if err == nil {
tariff, err = configureTariffs(conf.Tariffs)
}

if err == nil {
site, err = configureSite(conf.Site, cp, loadPoints)
site, err = configureSite(conf.Site, cp, loadPoints, tariff)
}
}

return site, err
}

func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint) (*core.Site, error) {
site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints)
func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint, tariff api.Tariff) (*core.Site, error) {
site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints, tariff)
if err != nil {
return nil, fmt.Errorf("failed configuring site: %w", err)
}
Expand Down
17 changes: 8 additions & 9 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,14 +380,6 @@ func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) {
// if disabled we cannot be charging
power = 0
}
// TODO
// else if power > 0 && lp.Site.pvMeter != nil {
// // limit charge power to generation plus grid consumption/ minus grid delivery
// // as the charger cannot have consumed more than that
// // consumedPower := consumedPower(lp.pvPower, lp.batteryPower, lp.gridPower)
// consumedPower := lp.Site.consumedPower()
// power = math.Min(power, consumedPower)
// }

// handler only called if charge meter was replaced by dummy
lp.chargeMeter.(*wrapper.ChargeMeter).SetPower(power)
Expand Down Expand Up @@ -992,7 +984,7 @@ func (lp *LoadPoint) publishSoCAndRange() {
}

// Update is the main control function. It reevaluates meters and charger state
func (lp *LoadPoint) Update(sitePower float64) {
func (lp *LoadPoint) Update(sitePower float64, cheap bool) {
mode := lp.GetMode()
lp.publish("mode", mode)

Expand Down Expand Up @@ -1082,6 +1074,13 @@ func (lp *LoadPoint) Update(sitePower float64) {
required = true
}

// tariff
if cheap {
targetCurrent = lp.GetMaxCurrent()
lp.log.DEBUG.Printf("cheap tariff: %.3gA", targetCurrent)
required = true
}

// Sunny Home Manager
if lp.remoteControlled(RemoteSoftDisable) {
remoteDisabled = RemoteSoftDisable
Expand Down
28 changes: 14 additions & 14 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func TestUpdatePowerZero(t *testing.T) {
}

lp.Mode = tc.mode
lp.Update(0) // sitePower 0
lp.Update(0, false) // sitePower 0

ctrl.Finish()
}
Expand Down Expand Up @@ -404,36 +404,36 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
lp.Update(500)
lp.Update(500, false)

t.Log("charging above target - soc deactivates charger")
clock.Add(5 * time.Minute)
vehicle.EXPECT().SoC().Return(90.0, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(500)
lp.Update(500, false)

t.Log("deactivated charger changes status to B")
clock.Add(5 * time.Minute)
vehicle.EXPECT().SoC().Return(95.0, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
lp.Update(-5000)
lp.Update(-5000, false)

t.Log("soc has fallen below target - soc update prevented by timer")
clock.Add(5 * time.Minute)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
lp.Update(-5000)
lp.Update(-5000, false)

t.Log("soc has fallen below target - soc update timer expired")
clock.Add(pollInterval)
vehicle.EXPECT().SoC().Return(85.0, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(true).Return(nil)
lp.Update(-5000)
lp.Update(-5000, false)

ctrl.Finish()
}
Expand Down Expand Up @@ -473,14 +473,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
lp.Update(500)
lp.Update(500, false)

t.Log("switch off when disconnected")
clock.Add(5 * time.Minute)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusA, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(-3000)
lp.Update(-3000, false)

if lp.Mode != api.ModeOff {
t.Error("unexpected mode", lp.Mode)
Expand Down Expand Up @@ -541,46 +541,46 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(0.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1)
lp.Update(-1, false)

t.Log("at 1:00h charging at 5 kWh")
clock.Add(time.Hour)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1)
lp.Update(-1, false)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:00h stop charging at 5 kWh")
clock.Add(time.Second)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
lp.Update(-1)
lp.Update(-1, false)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:00h restart charging at 5 kWh")
clock.Add(time.Second)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1)
lp.Update(-1, false)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:30h continue charging at 7.5 kWh")
clock.Add(30 * time.Minute)
rater.EXPECT().ChargedEnergy().Return(7.5, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1)
lp.Update(-1, false)
expectCache("chargedEnergy", 7500.0)

t.Log("at 2:00h stop charging at 10 kWh")
clock.Add(30 * time.Minute)
rater.EXPECT().ChargedEnergy().Return(10.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
lp.Update(-1)
lp.Update(-1, false)
expectCache("chargedEnergy", 10000.0)

ctrl.Finish()
Expand Down
12 changes: 10 additions & 2 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

// Updater abstracts the LoadPoint implementation for testing
type Updater interface {
Update(float64)
Update(float64, bool)
}

// Site is the main configuration container. A site can host multiple loadpoints.
Expand All @@ -42,6 +42,7 @@ type Site struct {
pvMeter api.Meter // PV generation meter
batteryMeter api.Meter // Battery charging meter

tariff api.Tariff // Tariff
loadpoints []*LoadPoint // Loadpoints

// cached state
Expand All @@ -63,13 +64,15 @@ func NewSiteFromConfig(
cp configProvider,
other map[string]interface{},
loadpoints []*LoadPoint,
tariff api.Tariff,
) (*Site, error) {
site := NewSite()
if err := util.DecodeOther(other, &site); err != nil {
return nil, err
}

Voltage = site.Voltage
site.tariff = tariff
site.loadpoints = loadpoints

if site.Meters.GridMeterRef != "" {
Expand Down Expand Up @@ -314,8 +317,13 @@ func (site *Site) sitePower() (float64, error) {
func (site *Site) update(lp Updater) {
site.log.DEBUG.Println("----")

var cheap bool
if site.tariff != nil {
cheap = site.tariff.IsCheap()
}

if sitePower, err := site.sitePower(); err == nil {
lp.Update(sitePower)
lp.Update(sitePower, cheap)
site.Health.Update()
}
}
Expand Down
9 changes: 9 additions & 0 deletions evcc.dist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ vehicles:
vin: WREN...
cache: 5m

# tariffs are the fixed or variable tariffs
# cheap can be used to define a tariff rate considered cheap enough for charging
tariffs:
grid:
type: tibber # or awattar or fixed
cheap: 20 # ct/kWh
token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4"
homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c"

# site describes the EVU connection, PV and home battery
site:
title: Home # display name for UI
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.29.0 // indirect
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.1.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU=
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
Expand Down
8 changes: 4 additions & 4 deletions mock/mock_loadpoint.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 986d734

Please sign in to comment.