Add SponsorToken configuration for Easee (#920)
This commit is contained in:
parent
ef06e9b489
commit
5681d1996b
26 changed files with 549 additions and 133 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -20,3 +20,4 @@ soc-server
|
|||
ca-cert.srl
|
||||
server-key.pem
|
||||
ca-key.pem
|
||||
cmd/wip
|
||||
|
|
|
|||
|
|
@ -29,14 +29,11 @@ func runCharger(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup mqtt
|
||||
if conf.Mqtt.Broker != "" {
|
||||
configureMQTT(conf.Mqtt)
|
||||
// setup environment
|
||||
if err := configureEnvironment(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
configureJavascript(conf.Javascript)
|
||||
|
||||
if err := cp.configureChargers(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,22 +14,23 @@ import (
|
|||
)
|
||||
|
||||
type config struct {
|
||||
URI string
|
||||
Log string
|
||||
Metrics bool
|
||||
Profile bool
|
||||
Levels map[string]string
|
||||
Interval time.Duration
|
||||
Mqtt mqttConfig
|
||||
Javascript map[string]interface{}
|
||||
Influx server.InfluxConfig
|
||||
HEMS typedConfig
|
||||
Messaging messagingConfig
|
||||
Meters []qualifiedConfig
|
||||
Chargers []qualifiedConfig
|
||||
Vehicles []qualifiedConfig
|
||||
Site map[string]interface{}
|
||||
LoadPoints []map[string]interface{}
|
||||
URI string
|
||||
Log string
|
||||
SponsorToken string
|
||||
Metrics bool
|
||||
Profile bool
|
||||
Levels map[string]string
|
||||
Interval time.Duration
|
||||
Mqtt mqttConfig
|
||||
Javascript map[string]interface{}
|
||||
Influx server.InfluxConfig
|
||||
HEMS typedConfig
|
||||
Messaging messagingConfig
|
||||
Meters []qualifiedConfig
|
||||
Chargers []qualifiedConfig
|
||||
Vehicles []qualifiedConfig
|
||||
Site map[string]interface{}
|
||||
LoadPoints []map[string]interface{}
|
||||
}
|
||||
|
||||
type mqttConfig struct {
|
||||
|
|
|
|||
11
cmd/dump.go
11
cmd/dump.go
|
|
@ -31,15 +31,12 @@ func runDump(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup mqtt
|
||||
if conf.Mqtt.Broker != "" {
|
||||
configureMQTT(conf.Mqtt)
|
||||
// setup environment
|
||||
if err := configureEnvironment(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
configureJavascript(conf.Javascript)
|
||||
|
||||
site, err := loadConfig(conf)
|
||||
site, err := configureSiteAndLoadpoints(conf)
|
||||
if err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,11 @@ func runMeter(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup mqtt
|
||||
if conf.Mqtt.Broker != "" {
|
||||
configureMQTT(conf.Mqtt)
|
||||
// setup environment
|
||||
if err := configureEnvironment(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
configureJavascript(conf.Javascript)
|
||||
|
||||
if err := cp.configureMeters(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
19
cmd/root.go
19
cmd/root.go
|
|
@ -152,13 +152,16 @@ func run(cmd *cobra.Command, args []string) {
|
|||
uri := viper.GetString("uri")
|
||||
log.INFO.Println("listening at", uri)
|
||||
|
||||
// setup mqtt client listener
|
||||
if conf.Mqtt.Broker != "" {
|
||||
configureMQTT(conf.Mqtt)
|
||||
// setup environment
|
||||
if err := configureEnvironment(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
configureJavascript(conf.Javascript)
|
||||
// setup loadpoints
|
||||
site, err := configureSiteAndLoadpoints(conf)
|
||||
if err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// start broadcasting values
|
||||
tee := &util.Tee{}
|
||||
|
|
@ -167,12 +170,6 @@ func run(cmd *cobra.Command, args []string) {
|
|||
cache := util.NewCache()
|
||||
go cache.Run(pipe.NewDropper(ignoreErrors...).Pipe(tee.Attach()))
|
||||
|
||||
// setup loadpoints
|
||||
site, err := loadConfig(conf)
|
||||
if err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup database
|
||||
if conf.Influx.URL != "" {
|
||||
configureDatabase(conf.Influx, site.LoadPoints(), tee.Attach())
|
||||
|
|
|
|||
84
cmd/setup.go
84
cmd/setup.go
|
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
|
@ -13,8 +14,11 @@ import (
|
|||
"github.com/andig/evcc/provider/mqtt"
|
||||
"github.com/andig/evcc/push"
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/andig/evcc/soc/proto/pb"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/cloud"
|
||||
"github.com/andig/evcc/util/pipe"
|
||||
"github.com/andig/evcc/util/sponsor"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
|
@ -24,6 +28,60 @@ func init() {
|
|||
|
||||
var cp = &ConfigProvider{}
|
||||
|
||||
func loadConfigFile(cfgFile string) (conf config, err error) {
|
||||
if cfgFile != "" {
|
||||
log.INFO.Println("using config file", cfgFile)
|
||||
if err := viper.UnmarshalExact(&conf); err != nil {
|
||||
log.FATAL.Fatalf("failed parsing config file %s: %v", cfgFile, err)
|
||||
}
|
||||
} else {
|
||||
err = errors.New("missing evcc config")
|
||||
}
|
||||
|
||||
return conf, err
|
||||
}
|
||||
|
||||
func configureEnvironment(conf config) (err error) {
|
||||
// setup sponsorship
|
||||
if conf.SponsorToken != "" {
|
||||
err = configureSponsorship(conf.SponsorToken)
|
||||
}
|
||||
|
||||
// setup mqtt client listener
|
||||
if err == nil && conf.Mqtt.Broker != "" {
|
||||
err = configureMQTT(conf.Mqtt)
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
if err == nil {
|
||||
err = configureJavascript(conf.Javascript)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func configureSponsorship(token string) error {
|
||||
host := util.Getenv("GRPC_URI", cloud.Host)
|
||||
conn, err := cloud.Connection(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := pb.NewAuthClient(conn)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := client.IsAuthorized(ctx, &pb.AuthRequest{Token: token})
|
||||
if err == nil {
|
||||
if res.Authorized {
|
||||
sponsor.Subject = res.Subject
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// setup influx databases
|
||||
func configureDatabase(conf server.InfluxConfig, loadPoints []core.LoadPointAPI, in <-chan util.Param) {
|
||||
influx := server.NewInfluxClient(
|
||||
|
|
@ -47,22 +105,25 @@ func configureDatabase(conf server.InfluxConfig, loadPoints []core.LoadPointAPI,
|
|||
}
|
||||
|
||||
// setup mqtt
|
||||
func configureMQTT(conf mqttConfig) {
|
||||
func configureMQTT(conf mqttConfig) error {
|
||||
log := util.NewLogger("mqtt")
|
||||
clientID := mqtt.ClientID()
|
||||
|
||||
var err error
|
||||
mqtt.Instance, err = mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, clientID, 1)
|
||||
if err != nil {
|
||||
log.FATAL.Fatalf("failed configuring mqtt: %v", err)
|
||||
return fmt.Errorf("failed configuring mqtt: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setup javascript
|
||||
func configureJavascript(conf map[string]interface{}) {
|
||||
func configureJavascript(conf map[string]interface{}) error {
|
||||
if err := javascript.Configure(conf); err != nil {
|
||||
log.FATAL.Fatalf("failed configuring javascript: %v", err)
|
||||
return fmt.Errorf("failed configuring javascript: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setup HEMS
|
||||
|
|
@ -93,7 +154,7 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Even
|
|||
return notificationChan
|
||||
}
|
||||
|
||||
func loadConfig(conf config) (site *core.Site, err error) {
|
||||
func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) {
|
||||
if err = cp.configure(conf); err == nil {
|
||||
var loadPoints []*core.LoadPoint
|
||||
loadPoints, err = configureLoadPoints(conf, cp)
|
||||
|
|
@ -138,16 +199,3 @@ func configureLoadPoints(conf config, cp *ConfigProvider) (loadPoints []*core.Lo
|
|||
|
||||
return loadPoints, nil
|
||||
}
|
||||
|
||||
func loadConfigFile(cfgFile string) (conf config, err error) {
|
||||
if cfgFile != "" {
|
||||
log.INFO.Println("using config file", cfgFile)
|
||||
if err := viper.UnmarshalExact(&conf); err != nil {
|
||||
log.FATAL.Fatalf("failed parsing config file %s: %v", cfgFile, err)
|
||||
}
|
||||
} else {
|
||||
err = errors.New("missing evcc config")
|
||||
}
|
||||
|
||||
return conf, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,11 @@ func runVehicle(cmd *cobra.Command, args []string) {
|
|||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup mqtt
|
||||
if conf.Mqtt.Broker != "" {
|
||||
configureMQTT(conf.Mqtt)
|
||||
// setup environment
|
||||
if err := configureEnvironment(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
||||
// setup javascript VMs
|
||||
configureJavascript(conf.Javascript)
|
||||
|
||||
if err := cp.configureVehicles(conf); err != nil {
|
||||
log.FATAL.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
uri: 0.0.0.0:7070 # uri for ui
|
||||
interval: 10s # control cycle interval
|
||||
|
||||
# sponsor token enables optional features
|
||||
# request sponsor token at https://cloud.evcc.io
|
||||
sponsortoken:
|
||||
|
||||
# log settings
|
||||
log: error
|
||||
levels:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/andig/evcc/internal/charger/easee"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/request"
|
||||
"github.com/andig/evcc/util/sponsor"
|
||||
)
|
||||
|
||||
// Easee charger implementation
|
||||
|
|
@ -49,7 +50,7 @@ func NewEaseeFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|||
func NewEasee(user, password, charger string, cache time.Duration) (*Easee, error) {
|
||||
log := util.NewLogger("easee")
|
||||
|
||||
if true {
|
||||
if !sponsor.IsAuthorized() {
|
||||
return nil, errors.New("easee requires evcc sponsorship, register at https://cloud.evcc.io")
|
||||
}
|
||||
|
||||
|
|
|
|||
16
internal/charger/easee/LICENSE
Normal file
16
internal/charger/easee/LICENSE
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
LICENSE
|
||||
|
||||
Copyright (c) 2019-2021 andig
|
||||
|
||||
This modules is NOT covered by the MIT license. All rights reserved.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -3,14 +3,13 @@ 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/cloud"
|
||||
"github.com/andig/evcc/util/request"
|
||||
)
|
||||
|
||||
|
|
@ -50,19 +49,18 @@ func NewCloudFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return nil, errors.New("missing required token")
|
||||
}
|
||||
|
||||
if host := os.Getenv("GRPC_URI"); host != "" {
|
||||
cloud.Host = host
|
||||
host := util.Getenv("GRPC_URI", cloud.Host)
|
||||
conn, err := cloud.Connection(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
client: pb.NewVehicleClient(conn),
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sponsortoken": {
|
||||
"type": "string"
|
||||
},
|
||||
"chargers": {
|
||||
"type": "array",
|
||||
"description": "List of chargers",
|
||||
|
|
|
|||
18
soc/proto/auth.proto
Normal file
18
soc/proto/auth.proto
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
syntax = "proto3";
|
||||
|
||||
// protoc proto/auth.proto --go_out=. --go-grpc_out=.
|
||||
|
||||
option go_package = "proto/pb";
|
||||
|
||||
service Auth {
|
||||
rpc IsAuthorized (AuthRequest) returns (AuthReply) {}
|
||||
}
|
||||
|
||||
message AuthRequest {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message AuthReply {
|
||||
bool authorized = 1;
|
||||
string subject = 2;
|
||||
}
|
||||
219
soc/proto/pb/auth.pb.go
Normal file
219
soc/proto/pb/auth.pb.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.15.8
|
||||
// source: proto/auth.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 AuthRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AuthRequest) Reset() {
|
||||
*x = AuthRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_auth_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AuthRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthRequest) ProtoMessage() {}
|
||||
|
||||
func (x *AuthRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_auth_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 AuthRequest.ProtoReflect.Descriptor instead.
|
||||
func (*AuthRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_auth_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AuthRequest) GetToken() string {
|
||||
if x != nil {
|
||||
return x.Token
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type AuthReply struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"`
|
||||
Subject string `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AuthReply) Reset() {
|
||||
*x = AuthReply{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_proto_auth_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AuthReply) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthReply) ProtoMessage() {}
|
||||
|
||||
func (x *AuthReply) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_auth_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 AuthReply.ProtoReflect.Descriptor instead.
|
||||
func (*AuthReply) Descriptor() ([]byte, []int) {
|
||||
return file_proto_auth_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *AuthReply) GetAuthorized() bool {
|
||||
if x != nil {
|
||||
return x.Authorized
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AuthReply) GetSubject() string {
|
||||
if x != nil {
|
||||
return x.Subject
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proto_auth_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_proto_auth_proto_rawDesc = []byte{
|
||||
0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x22, 0x23, 0x0a, 0x0b, 0x41, 0x75, 0x74, 0x68, 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, 0x22, 0x45, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x52,
|
||||
0x65, 0x70, 0x6c, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
|
||||
0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72,
|
||||
0x69, 0x7a, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x32, 0x32,
|
||||
0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68,
|
||||
0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x0c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, 0x2e, 0x41, 0x75, 0x74, 0x68, 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_auth_proto_rawDescOnce sync.Once
|
||||
file_proto_auth_proto_rawDescData = file_proto_auth_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_proto_auth_proto_rawDescGZIP() []byte {
|
||||
file_proto_auth_proto_rawDescOnce.Do(func() {
|
||||
file_proto_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_auth_proto_rawDescData)
|
||||
})
|
||||
return file_proto_auth_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_proto_auth_proto_goTypes = []interface{}{
|
||||
(*AuthRequest)(nil), // 0: AuthRequest
|
||||
(*AuthReply)(nil), // 1: AuthReply
|
||||
}
|
||||
var file_proto_auth_proto_depIdxs = []int32{
|
||||
0, // 0: Auth.IsAuthorized:input_type -> AuthRequest
|
||||
1, // 1: Auth.IsAuthorized:output_type -> AuthReply
|
||||
1, // [1:2] is the sub-list for method output_type
|
||||
0, // [0:1] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proto_auth_proto_init() }
|
||||
func file_proto_auth_proto_init() {
|
||||
if File_proto_auth_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_proto_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AuthRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_proto_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AuthReply); 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_auth_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_proto_auth_proto_goTypes,
|
||||
DependencyIndexes: file_proto_auth_proto_depIdxs,
|
||||
MessageInfos: file_proto_auth_proto_msgTypes,
|
||||
}.Build()
|
||||
File_proto_auth_proto = out.File
|
||||
file_proto_auth_proto_rawDesc = nil
|
||||
file_proto_auth_proto_goTypes = nil
|
||||
file_proto_auth_proto_depIdxs = nil
|
||||
}
|
||||
101
soc/proto/pb/auth_grpc.pb.go
Normal file
101
soc/proto/pb/auth_grpc.pb.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// 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
|
||||
|
||||
// AuthClient is the client API for Auth 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 AuthClient interface {
|
||||
IsAuthorized(ctx context.Context, in *AuthRequest, opts ...grpc.CallOption) (*AuthReply, error)
|
||||
}
|
||||
|
||||
type authClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAuthClient(cc grpc.ClientConnInterface) AuthClient {
|
||||
return &authClient{cc}
|
||||
}
|
||||
|
||||
func (c *authClient) IsAuthorized(ctx context.Context, in *AuthRequest, opts ...grpc.CallOption) (*AuthReply, error) {
|
||||
out := new(AuthReply)
|
||||
err := c.cc.Invoke(ctx, "/Auth/IsAuthorized", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AuthServer is the server API for Auth service.
|
||||
// All implementations must embed UnimplementedAuthServer
|
||||
// for forward compatibility
|
||||
type AuthServer interface {
|
||||
IsAuthorized(context.Context, *AuthRequest) (*AuthReply, error)
|
||||
mustEmbedUnimplementedAuthServer()
|
||||
}
|
||||
|
||||
// UnimplementedAuthServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedAuthServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedAuthServer) IsAuthorized(context.Context, *AuthRequest) (*AuthReply, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method IsAuthorized not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {}
|
||||
|
||||
// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AuthServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAuthServer interface {
|
||||
mustEmbedUnimplementedAuthServer()
|
||||
}
|
||||
|
||||
func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) {
|
||||
s.RegisterService(&Auth_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Auth_IsAuthorized_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AuthRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AuthServer).IsAuthorized(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/Auth/IsAuthorized",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AuthServer).IsAuthorized(ctx, req.(*AuthRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Auth_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "Auth",
|
||||
HandlerType: (*AuthServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "IsAuthorized",
|
||||
Handler: _Auth_IsAuthorized_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/auth.proto",
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.15.6
|
||||
// protoc v3.15.8
|
||||
// source: proto/vehicle.proto
|
||||
|
||||
package pb
|
||||
|
|
|
|||
|
|
@ -5,25 +5,25 @@ syntax = "proto3";
|
|||
option go_package = "proto/pb";
|
||||
|
||||
service Vehicle {
|
||||
rpc New (NewRequest) returns (NewReply) {}
|
||||
rpc SoC (SoCRequest) returns (SoCReply) {}
|
||||
rpc New (NewRequest) returns (NewReply) {}
|
||||
rpc SoC (SoCRequest) returns (SoCReply) {}
|
||||
}
|
||||
|
||||
message NewRequest {
|
||||
string token = 1;
|
||||
string type = 2;
|
||||
map<string,string> config = 3;
|
||||
string token = 1;
|
||||
string type = 2;
|
||||
map<string,string> config = 3;
|
||||
}
|
||||
|
||||
message NewReply {
|
||||
int64 vehicle_id = 1;
|
||||
int64 vehicle_id = 1;
|
||||
}
|
||||
|
||||
message SoCRequest {
|
||||
string token = 1;
|
||||
int64 vehicle_id = 2;
|
||||
string token = 1;
|
||||
int64 vehicle_id = 2;
|
||||
}
|
||||
|
||||
message SoCReply {
|
||||
double soc = 1;
|
||||
double soc = 1;
|
||||
}
|
||||
|
|
|
|||
40
soc/server/server/auth.go
Normal file
40
soc/server/server/auth.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/andig/evcc/soc/proto/pb"
|
||||
"github.com/andig/evcc/soc/server/auth"
|
||||
)
|
||||
|
||||
type AuthServer struct {
|
||||
pb.UnimplementedAuthServer
|
||||
}
|
||||
|
||||
func (s *AuthServer) IsAuthorized(ctx context.Context, r *pb.AuthRequest) (*pb.AuthReply, error) {
|
||||
authorized, _, claims, err := isAuthorized(r)
|
||||
|
||||
res := &pb.AuthReply{Authorized: authorized}
|
||||
if err == nil {
|
||||
res.Subject = claims.Subject
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
type tokenizer interface {
|
||||
GetToken() string
|
||||
}
|
||||
|
||||
func isAuthorized(r tokenizer) (bool, string, *auth.Claims, error) {
|
||||
token := r.GetToken()
|
||||
|
||||
claims, err := auth.ParseToken(token)
|
||||
if err != nil {
|
||||
return false, token, claims, err
|
||||
}
|
||||
|
||||
authorized, err := auth.IsAuthorized(claims.Subject)
|
||||
|
||||
return authorized, token, claims, err
|
||||
}
|
||||
|
|
@ -58,17 +58,16 @@ func Run() {
|
|||
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)
|
||||
|
||||
pb.RegisterVehicleServer(grpcServer, &VehicleServer{
|
||||
vehicles: make(map[string]map[int64]api.Vehicle),
|
||||
})
|
||||
pb.RegisterAuthServer(grpcServer, &AuthServer{})
|
||||
|
||||
log.Fatal(grpcServer.Serve(listener))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,44 +7,23 @@ import (
|
|||
|
||||
"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"
|
||||
"github.com/andig/evcc/util/cloud"
|
||||
)
|
||||
|
||||
var vehicleID int64
|
||||
|
||||
type Server struct {
|
||||
type VehicleServer 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) {
|
||||
func (s *VehicleServer) vehicle(r vehicler) (api.Vehicle, error) {
|
||||
token := r.GetToken()
|
||||
vehicles, ok := s.vehicles[token]
|
||||
if !ok {
|
||||
|
|
@ -70,11 +49,14 @@ func stringMapToInterface(in map[string]string) map[string]interface{} {
|
|||
return res
|
||||
}
|
||||
|
||||
func (s *Server) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) {
|
||||
token, claims, err := s.isAuthorized(r)
|
||||
func (s *VehicleServer) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error) {
|
||||
authorized, token, claims, err := isAuthorized(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !authorized {
|
||||
return nil, cloud.ErrNotAuthorized
|
||||
}
|
||||
|
||||
typ := r.GetType()
|
||||
config := r.GetConfig()
|
||||
|
|
@ -100,7 +82,7 @@ func (s *Server) New(ctx context.Context, r *pb.NewRequest) (*pb.NewReply, error
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
func (s *Server) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) {
|
||||
func (s *VehicleServer) SoC(ctx context.Context, r *pb.SoCRequest) (*pb.SoCReply, error) {
|
||||
v, err := s.vehicle(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -5,8 +5,6 @@ import (
|
|||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"github.com/andig/evcc/soc/proto/pb"
|
||||
"github.com/andig/evcc/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
|
@ -14,8 +12,7 @@ import (
|
|||
var Host = "cloud.evcc.io:8080"
|
||||
|
||||
var (
|
||||
conn *grpc.ClientConn
|
||||
client pb.VehicleClient
|
||||
conn *grpc.ClientConn
|
||||
)
|
||||
|
||||
func loadTLSCredentials() (*tls.Config, error) {
|
||||
|
|
@ -32,7 +29,7 @@ func loadTLSCredentials() (*tls.Config, error) {
|
|||
return config, nil
|
||||
}
|
||||
|
||||
func Client(log *util.Logger, uri string) (pb.VehicleClient, error) {
|
||||
func Connection(uri string) (*grpc.ClientConn, error) {
|
||||
var err error
|
||||
if conn == nil {
|
||||
var tlsConfig *tls.Config
|
||||
|
|
@ -48,9 +45,5 @@ func Client(log *util.Logger, uri string) (pb.VehicleClient, error) {
|
|||
conn, err = grpc.Dial(uri, transportOption)
|
||||
}
|
||||
|
||||
if client == nil && err == nil {
|
||||
client = pb.NewVehicleClient(conn)
|
||||
}
|
||||
|
||||
return client, err
|
||||
return conn, err
|
||||
}
|
||||
7
util/sponsor/auth.go
Normal file
7
util/sponsor/auth.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package sponsor
|
||||
|
||||
var Subject string
|
||||
|
||||
func IsAuthorized() bool {
|
||||
return len(Subject) > 0
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue