diff --git a/README.md b/README.md index d4bc713d0..e4a5d37de 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/api/api.go b/api/api.go index 681e8b52a..df12724f2 100644 --- a/api/api.go +++ b/api/api.go @@ -129,3 +129,7 @@ type VehicleStartCharge interface { type VehicleStopCharge interface { StopCharge() error } + +type Tariff interface { + IsCheap() bool +} diff --git a/cmd/config.go b/cmd/config.go index 360fc597e..c552f35e6 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -30,6 +30,7 @@ type config struct { Meters []qualifiedConfig Chargers []qualifiedConfig Vehicles []qualifiedConfig + Tariffs tariffConfig Site map[string]interface{} LoadPoints []map[string]interface{} } @@ -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 diff --git a/cmd/setup.go b/cmd/setup.go index 71e3a8811..7a94e2d18 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -8,6 +8,7 @@ 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" @@ -15,6 +16,7 @@ import ( "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" @@ -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 { - site, err = configureSite(conf.Site, cp, loadPoints) + tariff, err = configureTariffs(conf.Tariffs) + } + + if err == nil { + 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) } diff --git a/core/loadpoint.go b/core/loadpoint.go index 8d4a7232e..984624f46 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -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) @@ -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) @@ -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 diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index fa5b9d890..855bfc800 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -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() } @@ -404,7 +404,7 @@ 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) @@ -412,20 +412,20 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) { 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) @@ -433,7 +433,7 @@ func TestDisableAndEnableAtTargetSoC(t *testing.T) { 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() } @@ -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) @@ -541,14 +541,14 @@ 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") @@ -556,7 +556,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -564,7 +564,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -572,7 +572,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -580,7 +580,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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() diff --git a/core/site.go b/core/site.go index 17d18982b..7565f7ce6 100644 --- a/core/site.go +++ b/core/site.go @@ -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. @@ -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 @@ -63,6 +64,7 @@ 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 { @@ -70,6 +72,7 @@ func NewSiteFromConfig( } Voltage = site.Voltage + site.tariff = tariff site.loadpoints = loadpoints if site.Meters.GridMeterRef != "" { @@ -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() } } diff --git a/evcc.dist.yaml b/evcc.dist.yaml index cb9f035e4..f32f517b4 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -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 diff --git a/go.mod b/go.mod index e67b7ac93..12a24913b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 51165d4e7..abddcbc98 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/mock/mock_loadpoint.go b/mock/mock_loadpoint.go index ef8c3de62..0a1a50be6 100644 --- a/mock/mock_loadpoint.go +++ b/mock/mock_loadpoint.go @@ -34,13 +34,13 @@ func (m *MockUpdater) EXPECT() *MockUpdaterMockRecorder { } // Update mocks base method. -func (m *MockUpdater) Update(arg0 float64) { +func (m *MockUpdater) Update(arg0 float64, arg1 bool) { m.ctrl.T.Helper() - m.ctrl.Call(m, "Update", arg0) + m.ctrl.Call(m, "Update", arg0, arg1) } // Update indicates an expected call of Update. -func (mr *MockUpdaterMockRecorder) Update(arg0 interface{}) *gomock.Call { +func (mr *MockUpdaterMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1) } diff --git a/tariff/awattar.go b/tariff/awattar.go new file mode 100644 index 000000000..f0c52757f --- /dev/null +++ b/tariff/awattar.go @@ -0,0 +1,77 @@ +package tariff + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/tariff/awattar" + "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" +) + +type Awattar struct { + mux sync.Mutex + log *util.Logger + uri string + cheap float64 + data []awattar.PriceInfo +} + +var _ api.Tariff = (*Awattar)(nil) + +func NewAwattar(other map[string]interface{}) (*Awattar, error) { + cc := struct { + Cheap float64 + Region string + }{ + Region: "DE", + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + t := &Awattar{ + log: util.NewLogger("awattar"), + cheap: cc.Cheap, + uri: fmt.Sprintf(awattar.RegionURI, strings.ToLower(cc.Region)), + } + + go t.Run() + + return t, nil +} + +func (t *Awattar) Run() { + client := request.NewHelper(t.log) + + for ; true; <-time.NewTicker(time.Hour).C { + var res awattar.Prices + if err := client.GetJSON(t.uri, &res); err != nil { + t.log.ERROR.Println(err) + continue + } + + t.mux.Lock() + t.data = res.Data + t.mux.Unlock() + } +} + +func (t *Awattar) IsCheap() bool { + t.mux.Lock() + defer t.mux.Unlock() + + for i := len(t.data) - 1; i >= 0; i-- { + pi := t.data[i] + + if pi.StartTimestamp.Before(time.Now()) && pi.EndTimestamp.After(time.Now()) { + return pi.Marketprice/10 <= t.cheap // Eur/MWh conversion + } + } + + return false +} diff --git a/tariff/awattar/api.go b/tariff/awattar/api.go new file mode 100644 index 000000000..2b740ada5 --- /dev/null +++ b/tariff/awattar/api.go @@ -0,0 +1,38 @@ +package awattar + +import ( + "encoding/json" + "time" +) + +const RegionURI = "https://api.awattar.%s/v1/marketdata" + +type Prices struct { + Data []PriceInfo +} + +type PriceInfo struct { + StartTimestamp time.Time `json:"start_timestamp"` + EndTimestamp time.Time `json:"end_timestamp"` + Marketprice float64 `json:"marketprice"` + Unit string `json:"unit"` +} + +func (p *PriceInfo) UnmarshalJSON(data []byte) error { + var s struct { + StartTimestamp int64 `json:"start_timestamp"` + EndTimestamp int64 `json:"end_timestamp"` + Marketprice float64 `json:"marketprice"` + Unit string `json:"unit"` + } + + err := json.Unmarshal(data, &s) + if err == nil { + p.StartTimestamp = time.Unix(s.StartTimestamp/1e3, 0) + p.EndTimestamp = time.Unix(s.EndTimestamp/1e3, 0) + p.Marketprice = s.Marketprice + p.Unit = s.Unit + } + + return err +} diff --git a/tariff/config.go b/tariff/config.go new file mode 100644 index 000000000..be804b16f --- /dev/null +++ b/tariff/config.go @@ -0,0 +1,24 @@ +package tariff + +import ( + "errors" + "strings" + + "github.com/andig/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) + } + + return +} diff --git a/tariff/fixed.go b/tariff/fixed.go new file mode 100644 index 000000000..27b6edede --- /dev/null +++ b/tariff/fixed.go @@ -0,0 +1,26 @@ +package tariff + +import ( + "github.com/andig/evcc/api" + "github.com/andig/evcc/util" +) + +type Fixed struct { + Price float64 +} + +var _ api.Tariff = (*Fixed)(nil) + +func NewFixed(other map[string]interface{}) (*Fixed, error) { + cc := Fixed{} + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return &cc, nil +} + +func (t *Fixed) IsCheap() bool { + return false +} diff --git a/tariff/tibber.go b/tariff/tibber.go new file mode 100644 index 000000000..a722dc09c --- /dev/null +++ b/tariff/tibber.go @@ -0,0 +1,113 @@ +package tariff + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/tariff/tibber" + "github.com/andig/evcc/util" + "github.com/andig/evcc/util/request" + "github.com/shurcooL/graphql" + "golang.org/x/oauth2" +) + +type Tibber struct { + mux sync.Mutex + log *util.Logger + Token string + HomeID string + Cheap float64 + client *graphql.Client + data []tibber.PriceInfo +} + +var _ api.Tariff = (*Tibber)(nil) + +func NewTibber(other map[string]interface{}) (*Tibber, error) { + t := &Tibber{ + log: util.NewLogger("tibber"), + } + + if err := util.DecodeOther(other, &t); err != nil { + return nil, err + } + + ctx := context.WithValue( + context.Background(), + oauth2.HTTPClient, + request.NewHelper(t.log).Client, + ) + + client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: t.Token, + })) + + t.client = graphql.NewClient(tibber.URI, client) + + if t.HomeID == "" { + var res struct { + Viewer struct { + Homes []tibber.Home + } + } + + if err := t.client.Query(context.Background(), &res, nil); err != nil { + return nil, err + } + + if len(res.Viewer.Homes) != 1 { + return nil, fmt.Errorf("could not determine home id: %v", res.Viewer.Homes) + } + + t.HomeID = res.Viewer.Homes[0].ID + } + + go t.Run() + + return t, nil +} + +func (t *Tibber) Run() { + for ; true; <-time.NewTicker(time.Hour).C { + var res struct { + Viewer struct { + Home struct { + ID string + TimeZone string + CurrentSubscription tibber.Subscription + } `graphql:"home(id: $id)"` + } + } + + v := map[string]interface{}{ + "id": graphql.ID(t.HomeID), + } + + if err := t.client.Query(context.Background(), &res, v); err != nil { + t.log.ERROR.Println(err) + continue + } + + t.mux.Lock() + t.data = res.Viewer.Home.CurrentSubscription.PriceInfo.Today + t.mux.Unlock() + } +} + +func (t *Tibber) IsCheap() bool { + t.mux.Lock() + defer t.mux.Unlock() + + for i := len(t.data) - 1; i >= 0; i-- { + pi := t.data[i] + + if pi.StartsAt.Before(time.Now()) { + return pi.Total <= t.Cheap + } + } + + return false +} diff --git a/tariff/tibber/api.go b/tariff/tibber/api.go new file mode 100644 index 000000000..ff2a749a5 --- /dev/null +++ b/tariff/tibber/api.go @@ -0,0 +1,37 @@ +package tibber + +import ( + "time" +) + +const URI = "https://api.tibber.com/v1-beta/gql" + +type Home struct { + ID string + TimeZone string + Address Address + MeteringPointData struct { + GridCompany string + } +} + +type Address struct { + Address1, PostalCode, City, Country string +} + +type Subscription struct { + ID string + Status string + PriceInfo struct { + Current PriceInfo + Today []PriceInfo + // Tomorrow []PriceInfo + } +} + +type PriceInfo struct { + Level string + StartsAt time.Time + Total float64 + // Energy, Tax float64 +}