290 lines
6.6 KiB
Go
290 lines
6.6 KiB
Go
package eebus
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"dario.cat/mergo"
|
|
"github.com/enbility/cemd/cem"
|
|
"github.com/enbility/cemd/emobility"
|
|
"github.com/enbility/eebus-go/service"
|
|
"github.com/enbility/eebus-go/spine/model"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/machine"
|
|
)
|
|
|
|
const (
|
|
EEBUSBrandName string = "EVCC"
|
|
EEBUSModel string = "HEMS"
|
|
EEBUSDeviceCode string = "EVCC_HEMS_01" // used as common name in cert generation
|
|
)
|
|
|
|
type Config struct {
|
|
URI string
|
|
ShipID string
|
|
Interfaces []string
|
|
Certificate struct {
|
|
Public, Private []byte
|
|
}
|
|
}
|
|
|
|
type EEBusClientCBs struct {
|
|
onConnect func(string) // , ship.Conn) error
|
|
onDisconnect func(string)
|
|
}
|
|
|
|
type EEBus struct {
|
|
Cem *cem.CemImpl
|
|
|
|
mux sync.Mutex
|
|
log *util.Logger
|
|
|
|
SKI string
|
|
|
|
clients map[string]EEBusClientCBs
|
|
}
|
|
|
|
var Instance *EEBus
|
|
|
|
func NewServer(other Config) (*EEBus, error) {
|
|
cc := Config{
|
|
URI: ":4712",
|
|
}
|
|
|
|
if err := mergo.Merge(&cc, other, mergo.WithOverride); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log := util.NewLogger("eebus")
|
|
|
|
protectedID, err := machine.ProtectedID("evcc-eebus")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serial := fmt.Sprintf("%s-%0x", "EVCC", protectedID[:8])
|
|
|
|
if len(cc.ShipID) != 0 {
|
|
serial = cc.ShipID
|
|
}
|
|
|
|
certificate, err := tls.X509KeyPair(cc.Certificate.Public, cc.Certificate.Private)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, portValue, err := net.SplitHostPort(cc.URI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
port, err := strconv.Atoi(portValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: get the voltage from the site
|
|
configuration, err := service.NewConfiguration(
|
|
EEBUSBrandName, EEBUSBrandName, EEBUSModel, serial,
|
|
model.DeviceTypeTypeEnergyManagementSystem, port, certificate, 230,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// for backward compatibility
|
|
configuration.SetAlternateMdnsServiceName("EVCC_HEMS_01")
|
|
configuration.SetAlternateIdentifier(serial)
|
|
configuration.SetInterfaces(cc.Interfaces)
|
|
configuration.SetRegisterAutoAccept(true)
|
|
|
|
ski, err := SkiFromCert(certificate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &EEBus{
|
|
log: log,
|
|
clients: make(map[string]EEBusClientCBs),
|
|
SKI: ski,
|
|
}
|
|
|
|
c.Cem = cem.NewCEM(configuration, c, c)
|
|
if err := c.Cem.Setup(); err != nil {
|
|
return nil, err
|
|
}
|
|
c.Cem.EnableEmobility(emobility.EmobilityConfiguration{
|
|
CoordinatedChargingEnabled: false,
|
|
})
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (c *EEBus) RegisterEVSE(ski, ip string, connectHandler func(string), disconnectHandler func(string), dataProvider emobility.EmobilityDataProvider) *emobility.EMobilityImpl {
|
|
ski = strings.ReplaceAll(ski, "-", "")
|
|
ski = strings.ReplaceAll(ski, " ", "")
|
|
ski = strings.ToLower(ski)
|
|
c.log.TRACE.Printf("registering ski: %s", ski)
|
|
|
|
if ski == c.SKI {
|
|
c.log.FATAL.Fatal("The charger SKI can not be identical to the SKI of evcc!")
|
|
}
|
|
|
|
serviceDetails := service.NewServiceDetails(ski)
|
|
serviceDetails.SetIPv4(ip)
|
|
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
c.clients[ski] = EEBusClientCBs{onConnect: connectHandler, onDisconnect: disconnectHandler}
|
|
|
|
return c.Cem.RegisterEmobilityRemoteDevice(serviceDetails, dataProvider)
|
|
}
|
|
|
|
func (c *EEBus) Run() {
|
|
c.Cem.Start()
|
|
}
|
|
|
|
func (c *EEBus) Shutdown() {
|
|
c.Cem.Shutdown()
|
|
}
|
|
|
|
// EEBUSServiceHandler
|
|
|
|
// report the Ship ID of a newly trusted connection
|
|
func (c *EEBus) RemoteServiceShipIDReported(service *service.EEBUSService, ski string, shipID string) {
|
|
// we should associated the Ship ID with the SKI and store it
|
|
// so the next connection can start trusted
|
|
c.log.DEBUG.Println("SKI", ski, "has Ship ID:", shipID)
|
|
}
|
|
|
|
func (c *EEBus) RemoteSKIConnected(service *service.EEBUSService, ski string) {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
|
|
client, exists := c.clients[ski]
|
|
if !exists {
|
|
return
|
|
}
|
|
client.onConnect(ski)
|
|
}
|
|
|
|
func (c *EEBus) RemoteSKIDisconnected(service *service.EEBUSService, ski string) {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
|
|
client, exists := c.clients[ski]
|
|
if !exists {
|
|
return
|
|
}
|
|
client.onDisconnect(ski)
|
|
}
|
|
|
|
func (h *EEBus) ReportServiceShipID(ski string, shipdID string) {}
|
|
|
|
// EEBUS Logging interface
|
|
|
|
func (c *EEBus) Trace(args ...interface{}) {
|
|
c.log.TRACE.Println(args...)
|
|
}
|
|
|
|
func (c *EEBus) Tracef(format string, args ...interface{}) {
|
|
c.log.TRACE.Printf(format, args...)
|
|
}
|
|
|
|
func (c *EEBus) Debug(args ...interface{}) {
|
|
c.log.DEBUG.Println(args...)
|
|
}
|
|
|
|
func (c *EEBus) Debugf(format string, args ...interface{}) {
|
|
c.log.DEBUG.Printf(format, args...)
|
|
}
|
|
|
|
func (c *EEBus) Info(args ...interface{}) {
|
|
c.log.INFO.Println(args...)
|
|
}
|
|
|
|
func (c *EEBus) Infof(format string, args ...interface{}) {
|
|
c.log.INFO.Printf(format, args...)
|
|
}
|
|
|
|
func (c *EEBus) Error(args ...interface{}) {
|
|
c.log.ERROR.Println(args...)
|
|
}
|
|
|
|
func (c *EEBus) Errorf(format string, args ...interface{}) {
|
|
c.log.ERROR.Printf(format, args...)
|
|
}
|
|
|
|
// Certificate helpers
|
|
|
|
// CreateCertificate returns a newly created EEBUS compatible certificate
|
|
func CreateCertificate() (tls.Certificate, error) {
|
|
return service.CreateCertificate("", EEBUSBrandName, "DE", EEBUSDeviceCode)
|
|
}
|
|
|
|
// pemBlockForKey marshals private key into pem block
|
|
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
|
|
switch k := priv.(type) {
|
|
case *rsa.PrivateKey:
|
|
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
|
|
case *ecdsa.PrivateKey:
|
|
b, err := x509.MarshalECPrivateKey(k)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to marshal ECDSA private key: %w", err)
|
|
}
|
|
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
|
|
default:
|
|
return nil, errors.New("unknown private key type")
|
|
}
|
|
}
|
|
|
|
// GetX509KeyPair saves returns the cert and key string values
|
|
func GetX509KeyPair(cert tls.Certificate) (string, string, error) {
|
|
var certValue, keyValue string
|
|
|
|
out := new(bytes.Buffer)
|
|
err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]})
|
|
if err == nil {
|
|
certValue = out.String()
|
|
}
|
|
|
|
if len(certValue) > 0 {
|
|
var pb *pem.Block
|
|
if pb, err = pemBlockForKey(cert.PrivateKey); err == nil {
|
|
out.Reset()
|
|
err = pem.Encode(out, pb)
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
keyValue = out.String()
|
|
}
|
|
|
|
return certValue, keyValue, err
|
|
}
|
|
|
|
// SkiFromX509 extracts SKI from certificate
|
|
func skiFromX509(leaf *x509.Certificate) (string, error) {
|
|
if len(leaf.SubjectKeyId) == 0 {
|
|
return "", errors.New("missing SubjectKeyId")
|
|
}
|
|
return fmt.Sprintf("%0x", leaf.SubjectKeyId), nil
|
|
}
|
|
|
|
// SkiFromCert extracts SKI from certificate
|
|
func SkiFromCert(cert tls.Certificate) (string, error) {
|
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
|
if err != nil {
|
|
return "", errors.New("failed parsing certificate: " + err.Error())
|
|
}
|
|
return skiFromX509(leaf)
|
|
}
|