From 75470e8f5e39b2babcc48d2722bd0f76dfd032a5 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 17 Nov 2022 18:36:16 +0100 Subject: [PATCH] Add Tibber Pulse (#5185) --- go.mod | 1 + go.sum | 12 ++ meter/tibber-pulse.go | 160 +++++++++++++++++++ meter/tibber/types.go | 17 ++ meter/tplink.go | 1 - tariff/tibber.go | 39 +++-- templates/definition/meter/tibber-pulse.yaml | 19 +++ templates/docs/meter/tibber-pulse_0.yaml | 11 ++ templates/evcc.io/brands.json | 1 + 9 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 meter/tibber-pulse.go create mode 100644 meter/tibber/types.go create mode 100644 templates/definition/meter/tibber-pulse.yaml create mode 100644 templates/docs/meter/tibber-pulse_0.yaml diff --git a/go.mod b/go.mod index d7b1e23dc..bb582f902 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/gregdel/pushover v1.1.0 github.com/grid-x/modbus v0.0.0-20221109090852-6bf5f3bc63ea github.com/hashicorp/go-version v1.6.0 + github.com/hasura/go-graphql-client v0.8.1 github.com/imdario/mergo v0.3.13 github.com/influxdata/influxdb-client-go/v2 v2.12.0 github.com/itchyny/gojq v0.12.9 diff --git a/go.sum b/go.sum index f71b08577..9161ef143 100644 --- a/go.sum +++ b/go.sum @@ -276,6 +276,9 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -428,6 +431,10 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.4.0 h1:JE9wveRTSXwJyjdRd6bOQ7Ob5bewTUQ58Jv4OiVdpdE= +github.com/graph-gophers/graphql-go v1.4.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/graph-gophers/graphql-transport-ws v0.0.2 h1:DbmSkbIGzj8SvHei6n8Mh9eLQin8PtA8xY9eCzjRpvo= +github.com/graph-gophers/graphql-transport-ws v0.0.2/go.mod h1:5BVKvFzOd2BalVIBFfnfmHjpJi/MZ5rOj8G55mXvZ8g= github.com/gregdel/pushover v1.1.0 h1:dwHyvrcpZCOS9V1fAnKPaGRRI5OC55cVaKhMybqNsKQ= github.com/gregdel/pushover v1.1.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= github.com/grid-x/modbus v0.0.0-20210714071042-7af2b65ec03b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw= @@ -466,6 +473,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hasura/go-graphql-client v0.8.1 h1:yU4888urgkW4L47cs+QQDXl3YfVaNraUqym5qsJ41Ms= +github.com/hasura/go-graphql-client v0.8.1/go.mod h1:NVifIwv+YFIUYGLQ7SM2/vBbzS/9rFP4vmIf/vf/zXM= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -672,6 +681,7 @@ github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= @@ -886,6 +896,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= +go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= diff --git a/meter/tibber-pulse.go b/meter/tibber-pulse.go new file mode 100644 index 000000000..ce2f7ce40 --- /dev/null +++ b/meter/tibber-pulse.go @@ -0,0 +1,160 @@ +package meter + +import ( + "encoding/json" + "sync" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/meter/tibber" + "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/request" + "github.com/hasura/go-graphql-client" +) + +func init() { + registry.Add("tibber-pulse", NewTibberFromConfig) +} + +type Tibber struct { + mu sync.Mutex + log *util.Logger + updated time.Time + live tibber.LiveMeasurement +} + +func NewTibberFromConfig(other map[string]interface{}) (api.Meter, error) { + var cc struct { + Token string + HomeID string + } + + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + t := &Tibber{ + log: util.NewLogger("pulse").Redact(cc.HomeID, cc.Token), + } + + // TODO + // https://developer.tibber.com/docs/overview#breaking-websocket-change + // https://github.com/hasura/go-graphql-client/issues/38 + + // ctx := context.WithValue( + // context.Background(), + // oauth2.HTTPClient, + // request.NewHelper(t.log).Client, + // ) + + // client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + // AccessToken: t.Token, + // })) + + // urlClient := graphql.NewClient(tibber.URI, client) + + // var res struct { + // Viewer struct { + // WebsocketSubscriptionUrl string + // } + // } + + // if err := urlClient.Query(context.Background(), &res, nil); err != nil { + // panic(err) + // } + + // println(res.Viewer.WebsocketSubscriptionUrl) + + client := graphql.NewSubscriptionClient(tibber.SubscriptionURI). + WithConnectionParams(map[string]any{ + "token": cc.Token, + }). + WithLog(t.log.TRACE.Println) + + // Run the client + go func() { + if err := client.Run(); err != nil { + panic(err) + } + println("running") + }() + + // subscribe + var query struct { + tibber.LiveMeasurement `graphql:"liveMeasurement(homeId: $homeId)"` + } + + var once sync.Once + recv := make(chan struct{}) + errC := make(chan error) + + _, err := client.Subscribe(&query, map[string]any{ + "homeId": graphql.ID(cc.HomeID), + }, func(data []byte, err error) error { + if err != nil { + select { + case errC <- err: + default: + } + return nil + } + + var res struct { + LiveMeasurement tibber.LiveMeasurement + } + + if err := json.Unmarshal(data, &res); err != nil { + t.log.ERROR.Println(err) + return nil + } + + once.Do(func() { + close(recv) + }) + + t.mu.Lock() + t.live = res.LiveMeasurement + t.updated = time.Now() + t.mu.Unlock() + + return nil + }) + + // wait for connection + if err == nil { + select { + case <-recv: + case <-time.After(request.Timeout): + err = api.ErrTimeout + case err = <-errC: + } + } + + return t, err +} + +// CurrentPower implements the api.Meter interface +func (t *Tibber) CurrentPower() (float64, error) { + t.mu.Lock() + defer t.mu.Unlock() + + if time.Since(t.updated) > request.Timeout { + return 0, api.ErrTimeout + } + + return t.live.Power, nil +} + +var _ api.MeterCurrent = (*Tibber)(nil) + +// Currents implements the api.MeterCurrent interface +func (t *Tibber) Currents() (float64, float64, float64, error) { + t.mu.Lock() + defer t.mu.Unlock() + + if time.Since(t.updated) > request.Timeout { + return 0, 0, 0, api.ErrTimeout + } + + return t.live.CurrentL1, t.live.CurrentL2, t.live.CurrentL3, nil +} diff --git a/meter/tibber/types.go b/meter/tibber/types.go new file mode 100644 index 000000000..0a7713ec9 --- /dev/null +++ b/meter/tibber/types.go @@ -0,0 +1,17 @@ +package tibber + +const SubscriptionURI = "wss://api.tibber.com/v1-beta/gql/subscriptions" + +type LiveMeasurement struct { + // Timestamp time.Time + Power float64 + LastMeterConsumption float64 + LastMeterProduction float64 + CurrentL1, CurrentL2, CurrentL3 float64 + // Currency string + // AccumulatedConsumption float64 + // AccumulatedCost float64 + // MinPower float64 + // AveragePower float64 + // MaxPower float64 +} diff --git a/meter/tplink.go b/meter/tplink.go index 4c9c35e06..86b429d94 100644 --- a/meter/tplink.go +++ b/meter/tplink.go @@ -6,7 +6,6 @@ import ( "github.com/evcc-io/evcc/util" ) -// TP-Link meter implementation func init() { registry.Add("tplink", NewTPLinkFromConfig) } diff --git a/tariff/tibber.go b/tariff/tibber.go index 4da9ee685..5b54bbfb1 100644 --- a/tariff/tibber.go +++ b/tariff/tibber.go @@ -28,14 +28,14 @@ type Tibber struct { var _ api.Tariff = (*Tibber)(nil) func NewTibber(other map[string]interface{}) (*Tibber, error) { - t := &Tibber{ - log: util.NewLogger("tibber"), - } + t := &Tibber{} if err := util.DecodeOther(other, &t); err != nil { return nil, err } + t.log = util.NewLogger("tibber").Redact(t.HomeID, t.Token) + ctx := context.WithValue( context.Background(), oauth2.HTTPClient, @@ -49,21 +49,10 @@ func NewTibber(other map[string]interface{}) (*Tibber, error) { 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 { + var err error + if t.HomeID, err = t.DefaultHomeID(); 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() @@ -71,6 +60,24 @@ func NewTibber(other map[string]interface{}) (*Tibber, error) { return t, nil } +func (t *Tibber) DefaultHomeID() (string, error) { + var res struct { + Viewer struct { + Homes []tibber.Home + } + } + + if err := t.client.Query(context.Background(), &res, nil); err != nil { + return "", err + } + + if len(res.Viewer.Homes) != 1 { + return "", fmt.Errorf("could not determine home id: %v", res.Viewer.Homes) + } + + return res.Viewer.Homes[0].ID, nil +} + func (t *Tibber) Run() { for ; true; <-time.NewTicker(time.Hour).C { var res struct { diff --git a/templates/definition/meter/tibber-pulse.yaml b/templates/definition/meter/tibber-pulse.yaml new file mode 100644 index 000000000..8075ab0f5 --- /dev/null +++ b/templates/definition/meter/tibber-pulse.yaml @@ -0,0 +1,19 @@ +template: tibber-pulse +products: + - brand: Tibber + description: + generic: Pulse +params: + - name: usage + choice: ["grid"] + - name: token + mask: true + required: true + example: 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE + - name: homeid + required: true + example: 96a14971-525a-4420-aae9-e5aedaa129ff +render: | + type: tibber-pulse + token: {{ .token }} + homeid: {{ .homeid }} diff --git a/templates/docs/meter/tibber-pulse_0.yaml b/templates/docs/meter/tibber-pulse_0.yaml new file mode 100644 index 000000000..29fe4f303 --- /dev/null +++ b/templates/docs/meter/tibber-pulse_0.yaml @@ -0,0 +1,11 @@ +product: + brand: Tibber + description: Pulse +render: + - usage: grid + default: | + type: template + template: tibber-pulse + usage: grid + token: 5K4MVS-OjfWhK_4yrjOlFe1F6kJXPVf7eQYggo8ebAE + homeid: 96a14971-525a-4420-aae9-e5aedaa129ff diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json index 9ffbeb0ce..254cfb4c0 100644 --- a/templates/evcc.io/brands.json +++ b/templates/evcc.io/brands.json @@ -85,6 +85,7 @@ "Sonnen", "Sungrow", "Tesla", + "Tibber", "TQ", "VARTA", "Victron",