Add soc client/server implementation (#839)
This commit is contained in:
parent
995a1bb942
commit
fc14a0eb20
29 changed files with 1859 additions and 11 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -15,3 +15,8 @@ buildflags/*
|
|||
linux-*.Dockerfile
|
||||
node_modules
|
||||
release
|
||||
*.inc
|
||||
soc-server
|
||||
ca-cert.srl
|
||||
server-key.pem
|
||||
ca-key.pem
|
||||
|
|
|
|||
13
Makefile
13
Makefile
|
|
@ -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
13
go.mod
|
|
@ -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
43
go.sum
|
|
@ -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
118
internal/vehicle/cloud.go
Normal 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()
|
||||
}
|
||||
11
internal/vehicle/cloud/api.go
Normal file
11
internal/vehicle/cloud/api.go
Normal 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")
|
||||
)
|
||||
28
internal/vehicle/cloud/ca-cert.pem
Normal file
28
internal/vehicle/cloud/ca-cert.pem
Normal 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
internal/vehicle/cloud/cert.go
Normal file
14
internal/vehicle/cloud/cert.go
Normal 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()
|
||||
}
|
||||
56
internal/vehicle/cloud/client.go
Normal file
56
internal/vehicle/cloud/client.go
Normal 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
33
soc/Dockerfile
Normal 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
28
soc/cert/ca/ca-cert.pem
Normal 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
14
soc/cert/ca/cert.go
Normal 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
31
soc/cert/create.sh
Executable 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
28
soc/cert/server/cert.go
Normal 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)
|
||||
}
|
||||
29
soc/cert/server/server-cert.pem
Normal file
29
soc/cert/server/server-cert.pem
Normal 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-----
|
||||
1
soc/cert/server/server-ext.cnf
Normal file
1
soc/cert/server/server-ext.cnf
Normal file
|
|
@ -0,0 +1 @@
|
|||
subjectAltName=DNS:*.evcc.io,IP:0.0.0.0
|
||||
61
soc/client/main.go
Normal file
61
soc/client/main.go
Normal 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
373
soc/proto/pb/vehicle.pb.go
Normal 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
|
||||
}
|
||||
137
soc/proto/pb/vehicle_grpc.pb.go
Normal file
137
soc/proto/pb/vehicle_grpc.pb.go
Normal 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
29
soc/proto/vehicle.proto
Normal 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
36
soc/server/Dockerfile
Normal 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
99
soc/server/auth/auth.go
Normal 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
|
||||
}
|
||||
79
soc/server/auth/sponsor/sponsor.go
Normal file
79
soc/server/auth/sponsor/sponsor.go
Normal 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
18
soc/server/main.go
Normal 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
74
soc/server/server/init.go
Normal 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
115
soc/server/server/server.go
Normal 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
128
soc/server/ui/index.html
Normal 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
237
soc/server/ui/ui.go
Normal 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
19
util/env.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue