Add soc client/server implementation (#839)

This commit is contained in:
andig 2021-04-15 22:34:44 +02:00 • committed by GitHub
parent 995a1bb942
commit fc14a0eb20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1859 additions and 11 deletions

5
.gitignore vendored
View file

@ -15,3 +15,8 @@ buildflags/*
linux-*.Dockerfile
node_modules
release
*.inc
soc-server
ca-cert.srl
server-key.pem
ca-key.pem

View file

@ -1,6 +1,7 @@
.PHONY: default all clean install install-ui ui assets lint lint-ui test build test-release release
.PHONY: docker publish-testing publish-latest publish-images
.PHONY: prepare-image image-rootfs image-update
.PHONY: soc server server-image
# build vars
TAG_NAME := $(shell test -d .git && git describe --abbrev=0 --tags)
@ -98,3 +99,15 @@ image-rootfs:
image-update:
gokr-packer -update yes $(IMAGE_OPTIONS)
soc:
@echo Version: $(VERSION) $(BUILD_DATE)
go build -o evcc-soc $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/client
server:
@echo Version: $(VERSION) $(BUILD_DATE)
go build -o soc-server $(BUILD_TAGS) $(BUILD_ARGS) github.com/andig/evcc/soc/server
publish-server:
docker build -f soc/Dockerfile --platform linux/amd64 -t andig/evcc-cloud .
docker push andig/evcc-cloud

13
go.mod
View file

@ -11,6 +11,7 @@ require (
github.com/bogosj/tesla v0.0.0-20210326132323-e3261e5d6c70
github.com/containrrr/shoutrrr v0.4.0
github.com/denisbrodbeck/machineid v1.0.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dylanmei/iso8601 v0.1.0
github.com/eclipse/paho.mqtt.golang v1.3.1
github.com/fatih/structs v1.1.0
@ -19,7 +20,6 @@ require (
github.com/godbus/dbus/v5 v5.0.3
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24
github.com/golang/mock v1.4.4
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-github/v32 v32.1.0
github.com/google/uuid v1.1.5
github.com/gorilla/handlers v1.5.1
@ -47,6 +47,8 @@ require (
github.com/prometheus/common v0.10.0 // indirect
github.com/prometheus/procfs v0.2.0 // indirect
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa
github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect
github.com/spf13/cobra v1.1.1
github.com/spf13/jwalterweatherman v1.1.0
github.com/spf13/pflag v1.0.5
@ -54,9 +56,14 @@ require (
github.com/thoas/go-funk v0.7.0
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20210117183837-59dcc46d62d4
golang.org/x/net v0.0.0-20201216054612-986b41b23924
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/net v0.0.0-20210326220855-61e056675ecf
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93
golang.org/x/text v0.3.4
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 // indirect
golang.org/x/text v0.3.5
google.golang.org/genproto v0.0.0-20210325224202-eed09b1b5210 // indirect
google.golang.org/grpc v1.36.1
google.golang.org/protobuf v1.26.0
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

43
go.sum
View file

@ -87,6 +87,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containrrr/shoutrrr v0.4.0 h1:yy0Fbx1Z/gmfg66Xy1SGDrgmMLTcYCJQkqTM+mRb14M=
github.com/containrrr/shoutrrr v0.4.0/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@ -108,6 +109,7 @@ github.com/deepmap/oapi-codegen v1.4.1 h1:GAsp9N5SLYhQTKo+G4iZAJYzHBySpMh3ZP7Lml
github.com/deepmap/oapi-codegen v1.4.1/go.mod h1:1jY0YDxfBF3tXk1u3sARJMSUJa9wV0UrVT6o+2mr/zQ=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI=
@ -120,6 +122,7 @@ github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaB
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
@ -209,8 +212,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1 h1:jAbXjIeW2ZSW2AwFxlGTDoc2CjI2XujLkV3ArsZFCvc=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -220,8 +224,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
@ -241,6 +246,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@ -496,6 +502,10 @@ 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/githubv4 v0.0.0-20201206200315-234843c633fa h1:jozR3igKlnYCj9IVHOVump59bp07oIRoLQ/CcjMYIUA=
github.com/shurcooL/githubv4 v0.0.0-20201206200315-234843c633fa/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
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=
@ -619,6 +629,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
@ -669,8 +680,10 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210326220855-61e056675ecf h1:WUcCxqQqDT0aXO4VnQbfMvp4zh7m1Gb2clVuHUAGGRE=
golang.org/x/net v0.0.0-20210326220855-61e056675ecf/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -743,16 +756,22 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 h1:64ChN/hjER/taL4YJuA+gpLfIMT+/NFherRZixbxOhg=
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -804,8 +823,9 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2 h1:lHDhNNs7asPT3p01mm8EP3B+bNyyVfg0bcYjhJUYgxw=
golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -863,6 +883,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210325224202-eed09b1b5210 h1:fFxjezD+ZiiYJ6zyfH738tgcWOqfzWl9I1GoepZzrI4=
google.golang.org/genproto v0.0.0-20210325224202-eed09b1b5210/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -876,6 +898,9 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -885,8 +910,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

118
internal/vehicle/cloud.go Normal file
View file

@ -0,0 +1,118 @@
package vehicle
import (
"context"
"errors"
"os"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/vehicle/cloud"
"github.com/andig/evcc/provider"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/request"
)
// Cloud is an api.Vehicle implementation for Cloud cars
type Cloud struct {
*embed
token string
brand string
config map[string]string
client pb.VehicleClient
vehicleID int64
chargeStateG func() (float64, error)
}
func init() {
registry.Add("cloud", NewCloudFromConfig)
}
// NewCloudFromConfig creates a new vehicle
func NewCloudFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
Token string
Title string
Capacity int64
Brand string
Other map[string]string `mapstructure:",remain"`
Cache time.Duration
}{
Cache: interval,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if cc.Token == "" {
return nil, errors.New("missing required token")
}
if host := os.Getenv("GRPC_URI"); host != "" {
cloud.Host = host
}
log := util.NewLogger("cloud")
client, err := cloud.Client(log, cloud.Host)
v := &Cloud{
embed: &embed{cc.Title, cc.Capacity},
token: cc.Token,
brand: cc.Brand,
config: cc.Other,
client: client,
}
if err == nil {
err = v.prepareVehicle()
}
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
return v, err
}
// prepareVehicle obtains new vehicle handle from cloud server
func (v *Cloud) prepareVehicle() error {
req := &pb.NewRequest{
Token: v.token,
Type: v.brand,
Config: v.config,
}
ctx, cancel := context.WithTimeout(context.Background(), 3*request.Timeout)
defer cancel()
res, err := v.client.New(ctx, req)
if err == nil {
v.vehicleID = res.VehicleId
}
return err
}
// chargeState implements the api.Vehicle interface
func (v *Cloud) chargeState() (float64, error) {
req := &pb.SoCRequest{
Token: v.token,
VehicleId: v.vehicleID,
}
ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
defer cancel()
res, err := v.client.SoC(ctx, req)
if errors.As(err, &cloud.ErrVehicleNotAvailable) && v.prepareVehicle() == nil {
req.VehicleId = v.vehicleID
res, err = v.client.SoC(ctx, req)
}
return res.GetSoc(), err
}
// SoC implements the api.Vehicle interface
func (v *Cloud) SoC() (float64, error) {
return v.chargeStateG()
}

View file

@ -0,0 +1,11 @@
package cloud
import "errors"
var (
// ErrNotAuthorized indicates request token is not authorized
ErrNotAuthorized = errors.New("not authorized")
// ErrVehicleNotAvailable indicates vehicle not available, client should retry to prepare vehicle
ErrVehicleNotAvailable = errors.New("vehicle not available")
)

View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE1DCCArwCCQC/TbpAPhcOQTANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJE
RTEdMBsGCSqGSIb3DQEJARYOY3B1aWRsZUBnbXguZGUwHhcNMjEwMzMxMDc1ODAz
WhcNMjIwMzMxMDc1ODAzWjAsMQswCQYDVQQGEwJERTEdMBsGCSqGSIb3DQEJARYO
Y3B1aWRsZUBnbXguZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+
noDTXbidh/5RFfsLlMJbfMn4Vu91oJb4MM41RQM8UOr3SRU/ZzcoLfWJe2ePpEDp
ZL4piobs8EIjFcWU2C/8La0rygbcDDqUJkW/nViH0Drs5ctgJl/uYeiDLnZiSZO+
PBoOA0trXxqlJPxuEseJbFaLvVE81DsLBnltDPqhJZEUJPEUIjtfStOhQM0f5YoO
Greewk7P5LUkTAuTIz+xk6uKnC1YF89SKmdPMH26Rcy6q1IqhEQG3tjUliJZiKS9
eQlnp9gskqNGop9gEsmFIMnhlrTUsVU2q38zLDFcmNDbNkRvv4Tf+7jHo4tLs02m
/fMTikdMNCRMOcJKbdobcwXq4Ghvc85xeM4/8wIQHEDFbLzmxi/4ibFfJjOfQOe9
VpK+TbeSBTxBw42CH9V0TUOz/LELYkMrJ+zfwMCvTV4Eodmc0gjwiy9aIYdpZXKF
ueqGGljhVCq2XqkaSATFu5CsdPZrfUszLiAVqpuVTMjy3VQ9ICzz1Bd5ICBRFt6l
fDsvfB6SgmB9r1bC+yr/tqe/unzUPajL5mZwn7Jev0uOl6mBNmQpcIEne+9cKedf
vPCv/tUQpwpz6blGacEtTxuM+fWDk+CLpPnId/4PEnIQWRFE2aXQ4nrJSwu3O5+k
vuZNWety8MOSkuQr0toH//p3VNHOtT7L4JWSlDfTZQIDAQABMA0GCSqGSIb3DQEB
CwUAA4ICAQANoLbh4Di1A485ggnKfp9ykfVvg+NduGm+eqr27be033N6IJ7fuKmF
7Ki1E+sp0IJDMdyikbqoHdbroHu2chwp/1GzFxhx3Vo7kswS/ehSxWpHjhtrGjAF
tSDUFR9q4C+km2A3k1ZNyd9C1w+R9Lr16j9lBQoGWmUgFsRf8ED16FMMSK/1mCC0
1VSydYnYhKmPZxMByTozGTV97wihA0XqXtadbkQoyvcnEuarBvEX3mPA3effqcEG
rreeDp1yzYtQRW85ZASaqgF5CYKhe9NekurZ5Jd+2mQPYRWPFpIFsSHoOLqbfcN1
/S0Si6LZWq0Mi8cbSi/zq+Eh7Q0w+EXP6Goh0A7M4e1dKt+cfGQpeyeg3cF49Cda
RrTkSv2Sl2jbrShqAsG/HNhyBaI/gtLGi9tig9wAoV2zjGw1Ehs0GcwFq2g/f6Zv
W7rdZIBZT1RIQLczuwsEhv6cFrSM5OU2f5fuKnmeI6uvyz0jRqiMncYRjwI6LotB
sWmdTWi6xsctYirJ3Yip5Tqm01asyUuIeiT+eQ6I9CfHLiGBXEzxlK5PrFqfD7gK
LfB1gCN7SAlxKGepmFMwfF+fDrsurL2T2ePxaaNBLjRAzmpSpH0NedMd+eanuHXV
a/iOL2XmVis7iuFyk5M2XtLFfYBff+mxSPA8d9u5kCpoel0tx3iLmw==
-----END CERTIFICATE-----

View file

@ -0,0 +1,14 @@
package cloud
import (
"bytes"
_ "embed"
)
//go:embed ca-cert.pem
var caCert []byte
func caPEM() []byte {
copy := bytes.NewBuffer(caCert)
return copy.Bytes()
}

View file

@ -0,0 +1,56 @@
package cloud
import (
"crypto/tls"
"crypto/x509"
"fmt"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/util"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var Host = "cloud.evcc.io:8080"
var (
conn *grpc.ClientConn
client pb.VehicleClient
)
func loadTLSCredentials() (*tls.Config, error) {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(caPEM()) {
return nil, fmt.Errorf("failed to add CA certificate")
}
// create the credentials and return it
config := &tls.Config{
RootCAs: certPool,
}
return config, nil
}
func Client(log *util.Logger, uri string) (pb.VehicleClient, error) {
var err error
if conn == nil {
var tlsConfig *tls.Config
if tlsConfig, err = loadTLSCredentials(); err != nil {
return nil, fmt.Errorf("cannot load TLS credentials: %w", err)
}
transportOption := grpc.WithInsecure()
if tlsConfig != nil {
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
}
conn, err = grpc.Dial(uri, transportOption)
}
if client == nil && err == nil {
client = pb.NewVehicleClient(conn)
}
return client, err
}

33
soc/Dockerfile Normal file
View file

@ -0,0 +1,33 @@
# STEP 1 build executable binary
FROM golang:1.16-alpine as builder
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
RUN apk update && apk add --no-cache git ca-certificates tzdata alpine-sdk && update-ca-certificates
WORKDIR /build
# install go tools and cache modules
COPY Makefile .
COPY go.mod .
COPY go.sum .
RUN make install
RUN go mod download
# build
COPY . .
RUN make server
# STEP 2 build a small image including module support
FROM alpine:3.13
ENV TZ=Europe/Berlin
# Import from builder
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/soc-server /usr/local/bin/soc-server
CMD [ "soc-server" ]

28
soc/cert/ca/ca-cert.pem Normal file
View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE1DCCArwCCQC/TbpAPhcOQTANBgkqhkiG9w0BAQsFADAsMQswCQYDVQQGEwJE
RTEdMBsGCSqGSIb3DQEJARYOY3B1aWRsZUBnbXguZGUwHhcNMjEwMzMxMDc1ODAz
WhcNMjIwMzMxMDc1ODAzWjAsMQswCQYDVQQGEwJERTEdMBsGCSqGSIb3DQEJARYO
Y3B1aWRsZUBnbXguZGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+
noDTXbidh/5RFfsLlMJbfMn4Vu91oJb4MM41RQM8UOr3SRU/ZzcoLfWJe2ePpEDp
ZL4piobs8EIjFcWU2C/8La0rygbcDDqUJkW/nViH0Drs5ctgJl/uYeiDLnZiSZO+
PBoOA0trXxqlJPxuEseJbFaLvVE81DsLBnltDPqhJZEUJPEUIjtfStOhQM0f5YoO
Greewk7P5LUkTAuTIz+xk6uKnC1YF89SKmdPMH26Rcy6q1IqhEQG3tjUliJZiKS9
eQlnp9gskqNGop9gEsmFIMnhlrTUsVU2q38zLDFcmNDbNkRvv4Tf+7jHo4tLs02m
/fMTikdMNCRMOcJKbdobcwXq4Ghvc85xeM4/8wIQHEDFbLzmxi/4ibFfJjOfQOe9
VpK+TbeSBTxBw42CH9V0TUOz/LELYkMrJ+zfwMCvTV4Eodmc0gjwiy9aIYdpZXKF
ueqGGljhVCq2XqkaSATFu5CsdPZrfUszLiAVqpuVTMjy3VQ9ICzz1Bd5ICBRFt6l
fDsvfB6SgmB9r1bC+yr/tqe/unzUPajL5mZwn7Jev0uOl6mBNmQpcIEne+9cKedf
vPCv/tUQpwpz6blGacEtTxuM+fWDk+CLpPnId/4PEnIQWRFE2aXQ4nrJSwu3O5+k
vuZNWety8MOSkuQr0toH//p3VNHOtT7L4JWSlDfTZQIDAQABMA0GCSqGSIb3DQEB
CwUAA4ICAQANoLbh4Di1A485ggnKfp9ykfVvg+NduGm+eqr27be033N6IJ7fuKmF
7Ki1E+sp0IJDMdyikbqoHdbroHu2chwp/1GzFxhx3Vo7kswS/ehSxWpHjhtrGjAF
tSDUFR9q4C+km2A3k1ZNyd9C1w+R9Lr16j9lBQoGWmUgFsRf8ED16FMMSK/1mCC0
1VSydYnYhKmPZxMByTozGTV97wihA0XqXtadbkQoyvcnEuarBvEX3mPA3effqcEG
rreeDp1yzYtQRW85ZASaqgF5CYKhe9NekurZ5Jd+2mQPYRWPFpIFsSHoOLqbfcN1
/S0Si6LZWq0Mi8cbSi/zq+Eh7Q0w+EXP6Goh0A7M4e1dKt+cfGQpeyeg3cF49Cda
RrTkSv2Sl2jbrShqAsG/HNhyBaI/gtLGi9tig9wAoV2zjGw1Ehs0GcwFq2g/f6Zv
W7rdZIBZT1RIQLczuwsEhv6cFrSM5OU2f5fuKnmeI6uvyz0jRqiMncYRjwI6LotB
sWmdTWi6xsctYirJ3Yip5Tqm01asyUuIeiT+eQ6I9CfHLiGBXEzxlK5PrFqfD7gK
LfB1gCN7SAlxKGepmFMwfF+fDrsurL2T2ePxaaNBLjRAzmpSpH0NedMd+eanuHXV
a/iOL2XmVis7iuFyk5M2XtLFfYBff+mxSPA8d9u5kCpoel0tx3iLmw==
-----END CERTIFICATE-----

14
soc/cert/ca/cert.go Normal file
View file

@ -0,0 +1,14 @@
package ca
import (
"bytes"
_ "embed"
)
//go:embed ca-cert.pem
var cert []byte
func PEM() []byte {
copy := bytes.NewBuffer(cert)
return copy.Bytes()
}

31
soc/cert/create.sh Executable file
View file

@ -0,0 +1,31 @@
#!/bin/sh
set +ex
rm *.pem
# 1. Generate CA's private key and self-signed certificate
openssl req -x509 -newkey rsa:4096 -days 365 -nodes -keyout ca-key.pem -out ca-cert.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de"
echo "CA's self-signed certificate"
openssl x509 -in ca-cert.pem -noout -text
# 2. Generate web server's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout server-key.pem -out server-req.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de"
# 3. Use CA's private key to sign web server's CSR and get back the signed certificate
openssl x509 -req -in server-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile server-ext.cnf
echo "Server's signed certificate"
openssl x509 -in server-cert.pem -noout -text
# 4. Generate client's private key and certificate signing request (CSR)
openssl req -newkey rsa:4096 -nodes -keyout client-key.pem -out client-req.pem -subj "/C=DE/emailAddress=cpuidle@gmx.de"
# 5. Use CA's private key to sign client's CSR and get back the signed certificate
openssl x509 -req -in client-req.pem -days 60 -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -extfile client-ext.cnf
echo "Client's signed certificate"
openssl x509 -in client-cert.pem -noout -text
rm *-req.pem

28
soc/cert/server/cert.go Normal file
View file

@ -0,0 +1,28 @@
package server
import (
"bytes"
"crypto/tls"
_ "embed"
"github.com/andig/evcc/util"
)
//go:embed server-cert.pem
var cert []byte
//--go:embed server-key.pem
var key []byte
func init() {
key = []byte(util.Getenv("SERVER_KEY"))
}
func PEM() []byte {
copy := bytes.NewBuffer(cert)
return copy.Bytes()
}
func Certificate() (tls.Certificate, error) {
return tls.X509KeyPair(cert, key)
}

View file

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE+TCCAuGgAwIBAgIJALu7hrWUWtC8MA0GCSqGSIb3DQEBBQUAMCwxCzAJBgNV
BAYTAkRFMR0wGwYJKoZIhvcNAQkBFg5jcHVpZGxlQGdteC5kZTAeFw0yMTAzMzEw
NzU4MDVaFw0yMTA1MzAwNzU4MDVaMCwxCzAJBgNVBAYTAkRFMR0wGwYJKoZIhvcN
AQkBFg5jcHVpZGxlQGdteC5kZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
ggIBALzv8oy3yl+sH+7fezx6aDJWf77aCaZmBrQyupXmd8InJazRx8ScmpO+LJ7i
ZtVA1YuHbL2qZaV5SBk15UkRaVRRNqVitQmaRFYDrrAtDMosOhVNGOSorGnrlOqP
eu8LD/bWwZ57NqWE2z7ZTnqNNdgX1urcvzs7jmopKl4i71sKHAJ1jdzEYl+7l7r1
HdtKU4K6axQ2BBP6dHB9v0wOJQzfER76mSWe+hotCTdIiGnZhaIO5NoW31i3KkPi
277bLse5wGGbD1FvzLPaWf0HyruxARPs49kxuxnYtJ+KUDk5LXpDC6MM2QcUdV/8
HVkTadONW/gVFKWS7u21NrWH2342K0lz4FyNEwiQGmTCnyT4A9DQmdfWirnXsf4M
Jkag7fWqK/dEFIJK14RHZx+i53UBIoLu6mTT+MrHzdXRNewg9jlSwOYROG80DEMh
WIbTcYqUk9XPSUzzaeg2FuP7ZFEmpbsltsztaEQikd5PO9Dkl+Qzm5eiQMwNxJX5
bgb6eLQOI+HOtQHSWVTBXeM7Vf0JCfA7RAvKgRk4lK6JGijzGBSLuCLzXAPygiqb
59KgYf2EDaSmjzU+ZCLDR6l81SUDVnQj75obQt/TPBoVgwlCeOfFvLGGXY6tzJrp
8yH59MWpkRB4yQImTzmnoZ5klUjwuShi2bWLFiI0+FmXNuitAgMBAAGjHjAcMBoG
A1UdEQQTMBGCCSouZXZjYy5pb4cEAAAAADANBgkqhkiG9w0BAQUFAAOCAgEAlmh0
fR34BT1LTZcTZjFgk3a0/MaZ4zK8zdlwtQFHSlOh46t+TL+jps2bmLu9IlZxLF8E
lgSzLu5bWeP8H9HuBgji41RMtwvNUqSFUUMn08Dh/78lFISneqyJZ2CeRzwPYyYs
5xi7DBXitFbvtWj5GP3NQaDhsXAPoXtKXuqab3IE6eSwTJOxm5PJnvVdRTFSRQJW
W3qQFICQxMm6Oif+yCGZLgROj7sJWOOKJecevtAxYebOCWx5nxbXxAvOY3wiRerG
xsS3pT6Q5bVo5OxUjPeRb3Fe969JJEMqGaaGTGGtYP2IVkwarwzKBtcpwgG3NJYv
EYG/MEFkpBl6yYJRRB5D7EyPcBRcDHn53sJqnP+Y8Xq40tjfWhxZ9I+N8VWK9nwS
a9CKUHlwpB6aT/J1GNMiU2RXhQWgDv8qma6Vik7RH5ZmDMmtG18GERHTNdiGKk9b
v5xTphWMHl0NtcGPbPoc8PCrIVkb0frGAX5g6UVCHFfSkiUwLDV1zPhv/tLBa5hX
ZzW0wJzs6nDMj9NleyN1B1UY3mbyNKS2Ti9M9NBNEc+IEUdyj36RNmuvDmVNYTZ+
eYk28CFfkVT7XQjI1hIpBtu5JqBqic5Kzxg/Xwjmt1izpQBy8C7FFMb0Zs3uvrjo
FHk0eyJ/i830ZKCw8Ovx1fgMElZz8AZb/17aeRY=
-----END CERTIFICATE-----

View file

@ -0,0 +1 @@
subjectAltName=DNS:*.evcc.io,IP:0.0.0.0

61
soc/client/main.go Normal file
View file

@ -0,0 +1,61 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"github.com/andig/evcc/internal/vehicle"
"github.com/andig/evcc/util"
)
func usage() {
fmt.Print(`
soc
Usage:
soc vehicle [--log level] [--param value [...]]
`)
}
func main() {
if len(os.Args) < 3 {
usage()
log.Fatal("not enough arguments")
}
params := make(map[string]interface{})
params["brand"] = strings.ToLower(os.Args[1])
var key string
for _, arg := range os.Args[2:] {
if key == "" {
key = strings.ToLower(strings.TrimLeft(arg, "-"))
} else {
if key == "log" {
util.LogLevel(arg, nil)
} else {
params[key] = arg
}
key = ""
}
}
if key != "" {
usage()
log.Fatal("unexpected number of parameters")
}
v, err := vehicle.NewCloudFromConfig(params)
if err != nil {
log.Fatal(err)
}
soc, err := v.SoC()
if err != nil {
log.Fatal(err)
}
fmt.Println(soc)
}

373
soc/proto/pb/vehicle.pb.go Normal file
View file

@ -0,0 +1,373 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.15.6
// source: proto/vehicle.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type NewRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
Config map[string]string `protobuf:"bytes,3,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *NewRequest) Reset() {
*x = NewRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_vehicle_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NewRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NewRequest) ProtoMessage() {}
func (x *NewRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_vehicle_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NewRequest.ProtoReflect.Descriptor instead.
func (*NewRequest) Descriptor() ([]byte, []int) {
return file_proto_vehicle_proto_rawDescGZIP(), []int{0}
}
func (x *NewRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
func (x *NewRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *NewRequest) GetConfig() map[string]string {
if x != nil {
return x.Config
}
return nil
}
type NewReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
VehicleId int64 `protobuf:"varint,1,opt,name=vehicle_id,json=vehicleId,proto3" json:"vehicle_id,omitempty"`
}
func (x *NewReply) Reset() {
*x = NewReply{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_vehicle_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NewReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NewReply) ProtoMessage() {}
func (x *NewReply) ProtoReflect() protoreflect.Message {
mi := &file_proto_vehicle_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NewReply.ProtoReflect.Descriptor instead.
func (*NewReply) Descriptor() ([]byte, []int) {
return file_proto_vehicle_proto_rawDescGZIP(), []int{1}
}
func (x *NewReply) GetVehicleId() int64 {
if x != nil {
return x.VehicleId
}
return 0
}
type SoCRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
VehicleId int64 `protobuf:"varint,2,opt,name=vehicle_id,json=vehicleId,proto3" json:"vehicle_id,omitempty"`
}
func (x *SoCRequest) Reset() {
*x = SoCRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_vehicle_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SoCRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SoCRequest) ProtoMessage() {}
func (x *SoCRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_vehicle_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SoCRequest.ProtoReflect.Descriptor instead.
func (*SoCRequest) Descriptor() ([]byte, []int) {
return file_proto_vehicle_proto_rawDescGZIP(), []int{2}
}
func (x *SoCRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
func (x *SoCRequest) GetVehicleId() int64 {
if x != nil {
return x.VehicleId
}
return 0
}
type SoCReply struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Soc float64 `protobuf:"fixed64,1,opt,name=soc,proto3" json:"soc,omitempty"`
}
func (x *SoCReply) Reset() {
*x = SoCReply{}
if protoimpl.UnsafeEnabled {
mi := &file_proto_vehicle_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SoCReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SoCReply) ProtoMessage() {}
func (x *SoCReply) ProtoReflect() protoreflect.Message {
mi := &file_proto_vehicle_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SoCReply.ProtoReflect.Descriptor instead.
func (*SoCReply) Descriptor() ([]byte, []int) {
return file_proto_vehicle_proto_rawDescGZIP(), []int{3}
}
func (x *SoCReply) GetSoc() float64 {
if x != nil {
return x.Soc
}
return 0
}
var File_proto_vehicle_proto protoreflect.FileDescriptor
var file_proto_vehicle_proto_rawDesc = []byte{
0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x65, 0x68, 0x69, 0x63, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa2, 0x01, 0x0a, 0x0a, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79,
0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2f,
0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17,
0x2e, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66,
0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a,
0x39, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x29, 0x0a, 0x08, 0x4e, 0x65,
0x77, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x65, 0x68, 0x69, 0x63, 0x6c,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x76, 0x65, 0x68, 0x69,
0x63, 0x6c, 0x65, 0x49, 0x64, 0x22, 0x41, 0x0a, 0x0a, 0x53, 0x6f, 0x43, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x65, 0x68,
0x69, 0x63, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x76,
0x65, 0x68, 0x69, 0x63, 0x6c, 0x65, 0x49, 0x64, 0x22, 0x1c, 0x0a, 0x08, 0x53, 0x6f, 0x43, 0x52,
0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x6f, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28,
0x01, 0x52, 0x03, 0x73, 0x6f, 0x63, 0x32, 0x4b, 0x0a, 0x07, 0x56, 0x65, 0x68, 0x69, 0x63, 0x6c,
0x65, 0x12, 0x1f, 0x0a, 0x03, 0x4e, 0x65, 0x77, 0x12, 0x0b, 0x2e, 0x4e, 0x65, 0x77, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x4e, 0x65, 0x77, 0x52, 0x65, 0x70, 0x6c, 0x79,
0x22, 0x00, 0x12, 0x1f, 0x0a, 0x03, 0x53, 0x6f, 0x43, 0x12, 0x0b, 0x2e, 0x53, 0x6f, 0x43, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x53, 0x6f, 0x43, 0x52, 0x65, 0x70, 0x6c,
0x79, 0x22, 0x00, 0x42, 0x0a, 0x5a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x62, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_proto_vehicle_proto_rawDescOnce sync.Once
file_proto_vehicle_proto_rawDescData = file_proto_vehicle_proto_rawDesc
)
func file_proto_vehicle_proto_rawDescGZIP() []byte {
file_proto_vehicle_proto_rawDescOnce.Do(func() {
file_proto_vehicle_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_vehicle_proto_rawDescData)
})
return file_proto_vehicle_proto_rawDescData
}
var file_proto_vehicle_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_proto_vehicle_proto_goTypes = []interface{}{
(*NewRequest)(nil), // 0: NewRequest
(*NewReply)(nil), // 1: NewReply
(*SoCRequest)(nil), // 2: SoCRequest
(*SoCReply)(nil), // 3: SoCReply
nil, // 4: NewRequest.ConfigEntry
}
var file_proto_vehicle_proto_depIdxs = []int32{
4, // 0: NewRequest.config:type_name -> NewRequest.ConfigEntry
0, // 1: Vehicle.New:input_type -> NewRequest
2, // 2: Vehicle.SoC:input_type -> SoCRequest
1, // 3: Vehicle.New:output_type -> NewReply
3, // 4: Vehicle.SoC:output_type -> SoCReply
3, // [3:5] is the sub-list for method output_type
1, // [1:3] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proto_vehicle_proto_init() }
func file_proto_vehicle_proto_init() {
if File_proto_vehicle_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_proto_vehicle_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NewRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_vehicle_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NewReply); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_vehicle_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SoCRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_proto_vehicle_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SoCReply); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_vehicle_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_vehicle_proto_goTypes,
DependencyIndexes: file_proto_vehicle_proto_depIdxs,
MessageInfos: file_proto_vehicle_proto_msgTypes,
}.Build()
File_proto_vehicle_proto = out.File
file_proto_vehicle_proto_rawDesc = nil
file_proto_vehicle_proto_goTypes = nil
file_proto_vehicle_proto_depIdxs = nil
}

View file

@ -0,0 +1,137 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// VehicleClient is the client API for Vehicle service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type VehicleClient interface {
New(ctx context.Context, in *NewRequest, opts ...grpc.CallOption) (*NewReply, error)
SoC(ctx context.Context, in *SoCRequest, opts ...grpc.CallOption) (*SoCReply, error)
}
type vehicleClient struct {
cc grpc.ClientConnInterface
}
func NewVehicleClient(cc grpc.ClientConnInterface) VehicleClient {
return &vehicleClient{cc}
}
func (c *vehicleClient) New(ctx context.Context, in *NewRequest, opts ...grpc.CallOption) (*NewReply, error) {
out := new(NewReply)
err := c.cc.Invoke(ctx, "/Vehicle/New", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *vehicleClient) SoC(ctx context.Context, in *SoCRequest, opts ...grpc.CallOption) (*SoCReply, error) {
out := new(SoCReply)
err := c.cc.Invoke(ctx, "/Vehicle/SoC", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// VehicleServer is the server API for Vehicle service.
// All implementations must embed UnimplementedVehicleServer
// for forward compatibility
type VehicleServer interface {
New(context.Context, *NewRequest) (*NewReply, error)
SoC(context.Context, *SoCRequest) (*SoCReply, error)
mustEmbedUnimplementedVehicleServer()
}
// UnimplementedVehicleServer must be embedded to have forward compatible implementations.
type UnimplementedVehicleServer struct {
}
func (UnimplementedVehicleServer) New(context.Context, *NewRequest) (*NewReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method New not implemented")
}
func (UnimplementedVehicleServer) SoC(context.Context, *SoCRequest) (*SoCReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SoC not implemented")
}
func (UnimplementedVehicleServer) mustEmbedUnimplementedVehicleServer() {}
// UnsafeVehicleServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to VehicleServer will
// result in compilation errors.
type UnsafeVehicleServer interface {
mustEmbedUnimplementedVehicleServer()
}
func RegisterVehicleServer(s grpc.ServiceRegistrar, srv VehicleServer) {
s.RegisterService(&Vehicle_ServiceDesc, srv)
}
func _Vehicle_New_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NewRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VehicleServer).New(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Vehicle/New",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VehicleServer).New(ctx, req.(*NewRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Vehicle_SoC_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SoCRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(VehicleServer).SoC(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Vehicle/SoC",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(VehicleServer).SoC(ctx, req.(*SoCRequest))
}
return interceptor(ctx, in, info, handler)
}
// Vehicle_ServiceDesc is the grpc.ServiceDesc for Vehicle service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Vehicle_ServiceDesc = grpc.ServiceDesc{
ServiceName: "Vehicle",
HandlerType: (*VehicleServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "New",
Handler: _Vehicle_New_Handler,
},
{
MethodName: "SoC",
Handler: _Vehicle_SoC_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/vehicle.proto",
}

29
soc/proto/vehicle.proto Normal file
View file

@ -0,0 +1,29 @@
syntax = "proto3";
// protoc proto/vehicle.proto --go_out=. --go-grpc_out=.
option go_package = "proto/pb";
service Vehicle {
rpc New (NewRequest) returns (NewReply) {}
rpc SoC (SoCRequest) returns (SoCReply) {}
}
message NewRequest {
string token = 1;
string type = 2;
map<string,string> config = 3;
}
message NewReply {
int64 vehicle_id = 1;
}
message SoCRequest {
string token = 1;
int64 vehicle_id = 2;
}
message SoCReply {
double soc = 1;
}

36
soc/server/Dockerfile Normal file
View file

@ -0,0 +1,36 @@
# STEP 1 build executable binary
FROM golang:1.16-alpine as builder
# Install git + SSL ca certificates.
# Git is required for fetching the dependencies.
# Ca-certificates is required to call HTTPS endpoints.
RUN apk update && apk add --no-cache git ca-certificates tzdata alpine-sdk && update-ca-certificates
WORKDIR /build
# install go tools and cache modules
COPY go.mod .
COPY go.sum .
RUN go mod download
# prepare
COPY . .
# build
RUN make build
# STEP 3 build a small image including module support
FROM alpine:3.13
WORKDIR /app
ENV TZ=Europe/Berlin
# Import from builder
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/evcc-cloud /usr/local/bin/evcc-cloud
# ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD [ "evcc-cloud" ]

99
soc/server/auth/auth.go Normal file
View file

@ -0,0 +1,99 @@
package auth
import (
"context"
"errors"
"sync"
"time"
"github.com/andig/evcc/soc/server/auth/sponsor"
"github.com/andig/evcc/util"
"github.com/dgrijalva/jwt-go"
)
var (
TokenExpiry = 365 // days
tokenSecret = util.Getenv("JWT_TOKEN_SECRET")
)
var (
mux sync.Mutex
updated time.Time
authorized = make(map[string]bool)
)
const updateInterval = 5 * time.Minute
type Claims struct {
Username string `json:"username"`
jwt.StandardClaims
}
func keyFunc(token *jwt.Token) (interface{}, error) {
return []byte(tokenSecret), nil
}
func AuthorizedToken(name, login string) (string, error) {
expiry := time.Now().Add(24 * time.Hour * time.Duration(TokenExpiry))
claims := Claims{
Username: name,
StandardClaims: jwt.StandardClaims{
Subject: login,
ExpiresAt: expiry.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(tokenSecret))
}
func ParseToken(token string) (*Claims, error) {
jwt, err := jwt.ParseWithClaims(token, &Claims{}, keyFunc)
if err != nil {
return nil, err
}
if !jwt.Valid {
return nil, errors.New("token invalid")
}
claims, ok := jwt.Claims.(*Claims)
if !ok {
return nil, errors.New("token claims invalid")
}
return claims, nil
}
func IsAuthorized(login string) (bool, error) {
mux.Lock()
defer mux.Unlock()
if login == "demo" {
return true, nil
}
if _, ok := authorized[login]; !ok {
if time.Since(updated) < updateInterval {
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
all, err := sponsor.Get(ctx)
if err != nil {
return false, err
}
// fmt.Println("sponsors:", all)
updated = time.Now()
for _, s := range all {
authorized[s.Login] = true
}
}
_, ok := authorized[login]
return ok, nil
}

View file

@ -0,0 +1,79 @@
package sponsor
import (
"context"
"github.com/andig/evcc/util"
"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)
var client *githubv4.Client
func init() {
token := util.Getenv("GITHUB_TOKEN")
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(context.Background(), ts)
client = githubv4.NewClient(tc)
}
// Source: https://github.com/tj/sponsors-api/blob/master/server.go
// Sponsor model
type Sponsor struct {
Name string
Login string
AvatarURL string
}
// sponsorships query: https://docs.github.com/en/graphql/reference/objects
type sponsorships struct {
Viewer struct {
Login string
SponsorshipsAsMaintainer struct {
PageInfo struct {
EndCursor string
HasNextPage bool
}
Edges []struct {
Node struct {
Sponsor Sponsor
}
Cursor string
}
} `graphql:"sponsorshipsAsMaintainer(first: 100, after: $cursor, includePrivate: true)"`
}
}
// Get returns the list of sponsors
func Get(ctx context.Context) ([]Sponsor, error) {
var sponsors []Sponsor
var q sponsorships
var cursor string
for {
err := client.Query(ctx, &q, map[string]interface{}{
"cursor": githubv4.String(cursor),
})
if err != nil {
return nil, err
}
for _, edge := range q.Viewer.SponsorshipsAsMaintainer.Edges {
sponsor := edge.Node.Sponsor
sponsors = append(sponsors, sponsor)
}
if !q.Viewer.SponsorshipsAsMaintainer.PageInfo.HasNextPage {
break
}
cursor = q.Viewer.SponsorshipsAsMaintainer.PageInfo.EndCursor
}
return sponsors, nil
}

18
soc/server/main.go Normal file
View file

@ -0,0 +1,18 @@
package main
import (
"log"
compile "github.com/andig/evcc/server"
"github.com/andig/evcc/soc/server/server"
"github.com/andig/evcc/soc/server/ui"
)
func main() {
log.Printf("soc-server %s (%s)", compile.Version, compile.Commit)
go ui.Run()
go server.Run()
select {}
}

74
soc/server/server/init.go Normal file
View file

@ -0,0 +1,74 @@
package server
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"github.com/andig/evcc/api"
"github.com/andig/evcc/soc/cert/ca"
"github.com/andig/evcc/soc/cert/server"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/util"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var (
port = util.Getenv("GRPC_PORT", "8080")
tlsConfig *tls.Config
)
func init() {
var err error
if tlsConfig, err = loadTLSCredentials(); err != nil {
log.Fatalf("cannot load TLS credentials: %v", err)
}
}
func loadTLSCredentials() (*tls.Config, error) {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(ca.PEM()) {
return nil, fmt.Errorf("failed to add client CA's certificate")
}
// Load server's certificate and private key
serverCert, err := server.Certificate()
if err != nil {
return nil, err
}
// Create the credentials and return it
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: certPool,
}
return config, nil
}
func Run() {
fmt.Println("grpc:", ":"+port)
listener, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := &Server{
vehicles: make(map[string]map[int64]api.Vehicle),
}
serverOptions := []grpc.ServerOption{}
if tlsConfig != nil {
serverOptions = append(serverOptions, grpc.Creds(credentials.NewTLS(tlsConfig)))
}
grpcServer := grpc.NewServer(serverOptions...)
pb.RegisterVehicleServer(grpcServer, srv)
log.Fatal(grpcServer.Serve(listener))
}

115
soc/server/server/server.go Normal file
View file

@ -0,0 +1,115 @@
package server
import (
"context"
"fmt"
"sync/atomic"
"github.com/andig/evcc/api"
"github.com/andig/evcc/internal/vehicle"
"github.com/andig/evcc/internal/vehicle/cloud"
"github.com/andig/evcc/soc/proto/pb"
"github.com/andig/evcc/soc/server/auth"
)
var vehicleID int64
type Server struct {
vehicles map[string]map[int64]api.Vehicle
pb.UnimplementedVehicleServer
}
type tokenizer interface {
GetToken() string
}
func (s *Server) isAuthorized(r tokenizer) (string, *auth.Claims, error) {
token := r.GetToken()
claims, err := auth.ParseToken(token)
if err != nil {
return token, claims, err
}
authorized, err := auth.IsAuthorized(claims.Subject)
if err == nil && !authorized {
err = cloud.ErrNotAuthorized
}
return token, claims, err
}
type vehicler interface {
tokenizer
GetVehicleId() int64
}
func (s *Server) vehicle(r vehicler) (api.Vehicle, error) {
token := r.GetToken()
vehicles, ok := s.vehicles[token]
if !ok {
return nil, cloud.ErrVehicleNotAvailable
}
id := r.GetVehicleId()
v, ok := vehicles[id]
if !ok {
return nil, cloud.ErrVehicleNotAvailable
}
return v, nil
}
func stringMapToInterface(in map[string]string) map[string]interface{} {
res := make(map[string]interface{})
for k, v := range in {
res[k] = v
}
return res
}
func (s *Server) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) {
token, claims, err := s.isAuthorized(r)
if err != nil {
return nil, err
}
typ := r.GetType()
config := r.GetConfig()
// track vehicle create
fmt.Println(claims.Subject+":", typ)
v, err := vehicle.NewFromConfig(typ, stringMapToInterface(config))
if err != nil {
return nil, err
}
id := atomic.AddInt64(&vehicleID, 1)
if s.vehicles[token] == nil {
s.vehicles[token] = make(map[int64]api.Vehicle)
}
s.vehicles[token][id] = v
res := pb.NewReply{
VehicleId: id,
}
return &res, nil
}
func (s *Server) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) {
v, err := s.vehicle(r)
if err != nil {
return nil, err
}
soc, err := v.SoC()
res := pb.SoCReply{
Soc: soc,
}
return &res, err
}

128
soc/server/ui/index.html Normal file
View file

@ -0,0 +1,128 @@
<!doctype html>
<html lang="en" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>evcc cloud</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous">
</script>
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<style>
/* Custom default button */
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:focus {
color: #333;
text-shadow: none;
/* Prevent inheritance from `body` */
}
body {
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
}
.cover-container {
max-width: 42em;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
color: rgba(255, 255, 255, .5);
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .nav-link+.nav-link {
margin-left: 1rem;
}
.nav-masthead .active {
color: #fff;
border-bottom-color: #fff;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
</head>
<body class="d-flex h-100 text-center text-white bg-dark">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="mb-auto">
<div>
<h3 class="float-md-start mb-0">evcc cloud</h3>
<nav class="nav nav-masthead justify-content-center float-md-end">
<a class="nav-link active" aria-current="page" href="/">Home</a>
<a class="nav-link" href="#">Impressum</a>
</nav>
</div>
</header>
<main class="px-3">
{{if .Content}}
{{.Content}}
{{else}}
<h1>Willkommen</h1>
<p class="lead">
evcc cloud erfordert eine einmalige Anmeldung um evcc cloud mit dem Github Konto des evcc Sponsors zu
verknüpfen. Mit der Verknüpfung wird das Registrierungstoken erzeugt.
</p>
<p class="lead">
Die evcc cloud speichert keine Benutzerdaten. Alle Daten werden weiterhin lokal verarbeitet.
</p>
<p class="lead">
<a href="/login" class="btn btn-lg btn-secondary fw-bold border-white bg-white">Anmelden</a>
</p>
{{end}}
</main>
<footer class="mt-auto text-white-50">
<p>Cover template by <a href="https://twitter.com/mdo" class="text-white">@mdo</a>.</p>
</footer>
</div>
</body>
</html>

237
soc/server/ui/ui.go Normal file
View file

@ -0,0 +1,237 @@
package ui
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"math/rand"
"net/http"
"os"
"strings"
"time"
"github.com/andig/evcc/soc/server/auth"
"github.com/andig/evcc/util"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/oauth2"
"golang.org/x/oauth2/github"
)
//go:embed index.html
var indexHtml string
var (
// login ui and callback
redirectURL = util.Getenv("REDIRECT_URL")
// SSL termination
sslCertDir = util.Getenv("SSL_CERT_DIR", "certs")
// github oauth application
clientID = util.Getenv("GITHUB_CLIENT_ID")
clientSecret = util.Getenv("GITHUB_CLIENT_SECRET")
)
var (
indexTpl *template.Template
oauthState = randomString()
oauthConfig *oauth2.Config
)
const (
ProfileURL = "https://api.github.com/user"
EmailURL = "https://api.github.com/user/emails"
)
type User struct {
ID int `json:"id"`
Email string `json:"email"`
Bio string `json:"bio"`
Name string `json:"name"`
Login string `json:"login"`
Picture string `json:"avatar_url"`
Location string `json:"location"`
}
func init() {
clientID = util.Getenv("GITHUB_CLIENT_ID")
clientSecret = util.Getenv("GITHUB_CLIENT_SECRET")
oauthConfig = &oauth2.Config{
RedirectURL: redirectURL,
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: []string{},
Endpoint: github.Endpoint,
}
indexTpl = template.Must(template.New("index").Parse(indexHtml))
}
func randomString() string {
rand.Seed(time.Now().UnixNano())
chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
length := 16
var b strings.Builder
for i := 0; i < length; i++ {
b.WriteRune(chars[rand.Intn(len(chars))])
}
return b.String()
}
func handleMain(w http.ResponseWriter, r *http.Request) {
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(`
<h1>Willkommen</h1>
<p class="lead">
evcc cloud erfordert eine einmalige Anmeldung um evcc cloud mit dem Github Konto des evcc Sponsors zu
verknüpfen. Mit der Verknüpfung wird das Registrierungstoken erzeugt.
</p>
<p class="lead">
Die evcc cloud speichert keine Benutzerdaten. Alle Daten werden weiterhin lokal verarbeitet.
</p>
<p class="lead">
<a href="/login" class="btn btn-lg btn-secondary fw-bold border-white bg-white">Anmelden</a>
</p>`),
})
}
func templateError(w http.ResponseWriter, r *http.Request, err string) {
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(`
<h1>Fehler</h1>
<p class="lead">` + err + `</p>`),
})
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
url := oauthConfig.AuthCodeURL(oauthState)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func handleCallback(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("error") != "" {
err := r.URL.Query().Get("error_description")
templateError(w, r, err)
return
}
user, err := getUserInfo(r.FormValue("state"), r.FormValue("code"))
if err != nil {
templateError(w, r, err.Error())
return
}
authorized, err := auth.IsAuthorized(user.Login)
if err != nil {
templateError(w, r, err.Error())
return
}
authMap := map[bool]string{false: "rejected", true: "authorized"}
log.Println(user.Login+":", authMap[authorized])
if !authorized {
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(`
<h1>Inaktiv</h1>
<p class="lead">Github Sponsorship für evcc ist inaktiv. Um evcc cloud zu nutzen, ist es notwendig, evcc zu unterstützen.</p>
<p class="lead">evcc bei Github <a href="https://github.com/sponsors/andig" class="text-warning">unterstützen</a>.</p>`),
})
return
}
jwt, err := auth.AuthorizedToken(user.Name, user.Login)
if err != nil {
templateError(w, r, err.Error())
return
}
_ = indexTpl.Execute(w, map[string]interface{}{
"Content": template.HTML(fmt.Sprintf(`
<h1>Aktiv</h1>
<p class="lead">Github Sponsorship für evcc ist aktiv. Der folgende Token kann für den Zugriff auf evcc cloud genutzt werden.</p>
<p class="lead">Der Code ist %d Tage gültig und kann jederzeit neu erzeugt werden.</p>
<p class="lead"><code>`+jwt+`</code></p>`, auth.TokenExpiry),
)})
}
func getUserInfo(state string, code string) (*User, error) {
if state != oauthState {
return nil, fmt.Errorf("invalid oauth state")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
token, err := oauthConfig.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("code exchange failed: %s", err.Error())
}
var user User
req, err := http.NewRequest("GET", ProfileURL, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode)
}
bits, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(bits, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func Run() {
mux := &http.ServeMux{}
mux.HandleFunc("/", handleMain)
mux.HandleFunc("/login", handleLogin)
mux.HandleFunc("/callback", handleCallback)
s := &http.Server{
Addr: ":http",
Handler: mux,
}
if sslHosts := strings.TrimSpace(os.Getenv("SSL_HOSTS")); sslHosts != "" {
log.Println("sslHosts:", sslHosts)
log.Println("sslCertDir:", sslCertDir)
certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(strings.Split(sslHosts, " ")...),
Cache: autocert.DirCache(sslCertDir),
}
go log.Fatal(http.ListenAndServe(":http", certManager.HTTPHandler(nil)))
s.Addr = ":https"
s.TLSConfig = certManager.TLSConfig()
log.Fatal(s.ListenAndServeTLS("", ""))
} else {
log.Fatal(s.ListenAndServe())
}
}

19
util/env.go Normal file
View file

@ -0,0 +1,19 @@
package util
import (
"log"
"os"
"strings"
)
func Getenv(key string, def ...string) string {
res := strings.TrimSpace(os.Getenv(key))
if res == "" {
if len(def) == 1 {
return def[0]
}
log.Fatalln("missing", key)
}
return res
}