Skip to content

Commit 986d734

Browse files
authored
Add Awattar and Tibber (#1169)
1 parent d1ec6f6 commit 986d734

File tree

17 files changed

+414
-32
lines changed

17 files changed

+414
-32
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
3636
- [Meter](#meter)
3737
- [Vehicle](#vehicle)
3838
- [Home Energy Management System](#home-energy-management-system)
39+
- [Flexible Energy Tariffs](#flexible-energy-tariffs)
3940
- [Plugins](#plugins)
4041
- [Modbus (read/write)](#modbus-readwrite)
4142
- [MQTT (read/write)](#mqtt-readwrite)
@@ -295,6 +296,25 @@ to the configuration. The EVCC loadpoints can then be added to the SHM configura
295296
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
296297
to the left makes **Min+PV** behave as described above. Pushing completely to the right makes **Min+PV** mode behave like **PV** mode.
297298
299+
### Flexible Energy Tariffs
300+
301+
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:
302+
303+
```yaml
304+
tariffs:
305+
grid:
306+
# either
307+
type: tibber
308+
cheap: 20 # ct/kWh
309+
token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4" # access token
310+
homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c" # optional if multiple homes associated to account
311+
312+
# or
313+
type: awattar
314+
cheap: 20 # ct/kWh
315+
region: de # optional, chose at for Austria
316+
```
317+
298318
## Plugins
299319
300320
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.

api/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,7 @@ type VehicleStartCharge interface {
129129
type VehicleStopCharge interface {
130130
StopCharge() error
131131
}
132+
133+
type Tariff interface {
134+
IsCheap() bool
135+
}

cmd/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type config struct {
3030
Meters []qualifiedConfig
3131
Chargers []qualifiedConfig
3232
Vehicles []qualifiedConfig
33+
Tariffs tariffConfig
3334
Site map[string]interface{}
3435
LoadPoints []map[string]interface{}
3536
}
@@ -61,6 +62,10 @@ type messagingConfig struct {
6162
Services []typedConfig
6263
}
6364

65+
type tariffConfig struct {
66+
Grid typedConfig
67+
}
68+
6469
// ConfigProvider provides configuration items
6570
type ConfigProvider struct {
6671
meters map[string]api.Meter

cmd/setup.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import (
88
"strconv"
99
"time"
1010

11+
"github.com/andig/evcc/api"
1112
"github.com/andig/evcc/api/proto/pb"
1213
"github.com/andig/evcc/core"
1314
"github.com/andig/evcc/hems"
1415
"github.com/andig/evcc/provider/javascript"
1516
"github.com/andig/evcc/provider/mqtt"
1617
"github.com/andig/evcc/push"
1718
"github.com/andig/evcc/server"
19+
"github.com/andig/evcc/tariff"
1820
"github.com/andig/evcc/util"
1921
"github.com/andig/evcc/util/cloud"
2022
"github.com/andig/evcc/util/pipe"
@@ -165,21 +167,38 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Even
165167
return notificationChan
166168
}
167169

170+
func configureTariffs(conf tariffConfig) (t api.Tariff, err error) {
171+
if conf.Grid.Type != "" {
172+
t, err = tariff.NewFromConfig(conf.Grid.Type, conf.Grid.Other)
173+
}
174+
175+
if err != nil {
176+
err = fmt.Errorf("failed configuring tariff: %w", err)
177+
}
178+
179+
return t, err
180+
}
181+
168182
func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) {
169183
if err = cp.configure(conf); err == nil {
170184
var loadPoints []*core.LoadPoint
171185
loadPoints, err = configureLoadPoints(conf, cp)
172186

187+
var tariff api.Tariff
188+
if err == nil {
189+
tariff, err = configureTariffs(conf.Tariffs)
190+
}
191+
173192
if err == nil {
174-
site, err = configureSite(conf.Site, cp, loadPoints)
193+
site, err = configureSite(conf.Site, cp, loadPoints, tariff)
175194
}
176195
}
177196

178197
return site, err
179198
}
180199

181-
func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint) (*core.Site, error) {
182-
site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints)
200+
func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint, tariff api.Tariff) (*core.Site, error) {
201+
site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints, tariff)
183202
if err != nil {
184203
return nil, fmt.Errorf("failed configuring site: %w", err)
185204
}

core/loadpoint.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,6 @@ func (lp *LoadPoint) evChargeCurrentWrappedMeterHandler(current float64) {
380380
// if disabled we cannot be charging
381381
power = 0
382382
}
383-
// TODO
384-
// else if power > 0 && lp.Site.pvMeter != nil {
385-
// // limit charge power to generation plus grid consumption/ minus grid delivery
386-
// // as the charger cannot have consumed more than that
387-
// // consumedPower := consumedPower(lp.pvPower, lp.batteryPower, lp.gridPower)
388-
// consumedPower := lp.Site.consumedPower()
389-
// power = math.Min(power, consumedPower)
390-
// }
391383

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

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

@@ -1082,6 +1074,13 @@ func (lp *LoadPoint) Update(sitePower float64) {
10821074
required = true
10831075
}
10841076

1077+
// tariff
1078+
if cheap {
1079+
targetCurrent = lp.GetMaxCurrent()
1080+
lp.log.DEBUG.Printf("cheap tariff: %.3gA", targetCurrent)
1081+
required = true
1082+
}
1083+
10851084
// Sunny Home Manager
10861085
if lp.remoteControlled(RemoteSoftDisable) {
10871086
remoteDisabled = RemoteSoftDisable

core/loadpoint_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func TestUpdatePowerZero(t *testing.T) {
168168
}
169169

170170
lp.Mode = tc.mode
171-
lp.Update(0) // sitePower 0
171+
lp.Update(0, false) // sitePower 0
172172

173173
ctrl.Finish()
174174
}
@@ -404,36 +404,36 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) {
404404
charger.EXPECT().Status().Return(api.StatusC, nil)
405405
charger.EXPECT().Enabled().Return(lp.enabled, nil)
406406
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
407-
lp.Update(500)
407+
lp.Update(500, false)
408408

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

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

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

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

438438
ctrl.Finish()
439439
}
@@ -473,14 +473,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
473473
charger.EXPECT().Enabled().Return(lp.enabled, nil)
474474
charger.EXPECT().Status().Return(api.StatusC, nil)
475475
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
476-
lp.Update(500)
476+
lp.Update(500, false)
477477

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

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

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

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

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

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

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

586586
ctrl.Finish()

core/site.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
// Updater abstracts the LoadPoint implementation for testing
1919
type Updater interface {
20-
Update(float64)
20+
Update(float64, bool)
2121
}
2222

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

45+
tariff api.Tariff // Tariff
4546
loadpoints []*LoadPoint // Loadpoints
4647

4748
// cached state
@@ -63,13 +64,15 @@ func NewSiteFromConfig(
6364
cp configProvider,
6465
other map[string]interface{},
6566
loadpoints []*LoadPoint,
67+
tariff api.Tariff,
6668
) (*Site, error) {
6769
site := NewSite()
6870
if err := util.DecodeOther(other, &site); err != nil {
6971
return nil, err
7072
}
7173

7274
Voltage = site.Voltage
75+
site.tariff = tariff
7376
site.loadpoints = loadpoints
7477

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

320+
var cheap bool
321+
if site.tariff != nil {
322+
cheap = site.tariff.IsCheap()
323+
}
324+
317325
if sitePower, err := site.sitePower(); err == nil {
318-
lp.Update(sitePower)
326+
lp.Update(sitePower, cheap)
319327
site.Health.Update()
320328
}
321329
}

evcc.dist.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ vehicles:
5353
vin: WREN...
5454
cache: 5m
5555

56+
# tariffs are the fixed or variable tariffs
57+
# cheap can be used to define a tariff rate considered cheap enough for charging
58+
tariffs:
59+
grid:
60+
type: tibber # or awattar or fixed
61+
cheap: 20 # ct/kWh
62+
token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4"
63+
homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c"
64+
5665
# site describes the EVU connection, PV and home battery
5766
site:
5867
title: Home # display name for UI

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ require (
5959
github.com/prometheus/client_golang v1.11.0
6060
github.com/prometheus/common v0.29.0 // indirect
6161
github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452
62+
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a
6263
github.com/sirupsen/logrus v1.8.1 // indirect
6364
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
6465
github.com/spf13/cobra v1.1.3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
586586
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
587587
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
588588
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
589+
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU=
590+
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
589591
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
590592
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
591593
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

0 commit comments

Comments
 (0)