Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for controlling eebus-go via JSON-RPC from another process #128

Merged
merged 34 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f0ea56c
Add basic structure for a Remote interface to the EEBus use-cases
sthelen-enqs Jun 21, 2024
b5dcc34
Ensure jsonrpc packets are newline framed
sthelen-enqs Jul 1, 2024
d048fc2
Register LPC use case and propagate events as JSON-RPC notifications
sthelen-enqs Jul 2, 2024
587506e
Add functionality to automatically register exported methods
sthelen-enqs Jul 10, 2024
fd67f03
Add util for saving generated keys
sthelen-enqs Jul 16, 2024
1cbaf34
Cleanup rpc calling
sthelen-enqs Jul 16, 2024
1dd6fe8
Pass entityAddress as argument for notifications
sthelen-enqs Jul 16, 2024
15e3eae
Add Bind function to RpcServer to register new connections
sthelen-enqs Jul 18, 2024
8dcd8fe
Scope notification methods similar to calls
sthelen-enqs Jul 18, 2024
275470a
Unmarshal json parameters directly to function parameter slice
sthelen-enqs Jul 18, 2024
e524882
Provide device SKI in notifications to allow device mapping
sthelen-enqs Jul 18, 2024
c0d68ab
Ensure that we can scope use cases fully when registering.
sthelen-enqs Jul 18, 2024
ad79200
Lock access to entityInterfaces map
sthelen-enqs Jul 18, 2024
af880bb
Handle nullable parameters and functions returning (error, value)
sthelen-enqs Jul 18, 2024
0241dab
Add ConnectedDevices function to get list of connected RemotDevices
sthelen-enqs Jul 18, 2024
f366a83
Fix panic when EntityRemoteInterface parameter is null
sthelen-enqs Jul 18, 2024
57680f3
Add RemoteDeviceInfo function to wrap RemoteDeviceForSki
sthelen-enqs Jul 18, 2024
b1fbe82
Fix several possible nil dereferences
sthelen-enqs Jul 18, 2024
e2aec46
Check method calls for correct number of parameters
sthelen-enqs Jul 18, 2024
92a774c
Allow specifying mdns info via arguments
sthelen-enqs Jul 18, 2024
f34154e
If an explicit interface is specified, wait until the interface is
sthelen-enqs Jul 18, 2024
d8fc201
Map directly between AddressTypes and interfaces using appropriate
sthelen-enqs Jul 18, 2024
79e1236
Adjust callback for RemoteSKIConnected to return device address
sthelen-enqs Jul 18, 2024
93dabf9
Add basic log interface to rpcService
sthelen-enqs Jul 18, 2024
8e6e54b
Split out reflection api and add Call interface for device/entities
sthelen-enqs Jul 18, 2024
35fd334
Add SKI to all notifications
sthelen-enqs Jul 18, 2024
2bb5593
RemoteSKIDisconnected returns ski not device
sthelen-enqs Jul 18, 2024
dbca882
feat: enable missing ucs
sthelen-enqs Jul 18, 2024
132c1f3
Accept requests omitting the params field.
sthelen-enqs Jul 18, 2024
05f2a7f
added missing notifications to api.ServiceReaderInterface
Oct 17, 2024
0c940e9
Fix compilation after sync on dev
sthelen-enqs Jul 18, 2024
44c4778
Explicitly ignore the return type of conn.Notify
sthelen-enqs Jul 18, 2024
7abed76
Always return argument responses as complete array.
sthelen-enqs Jul 18, 2024
cf8b0d6
Forward potential errors from registerStaticReceiverProxy up through
sthelen-enqs Jul 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions cmd/remote/framer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"bufio"
"context"
"fmt"
"io"

"golang.org/x/exp/jsonrpc2"
)

type NewlineFramer struct{}
type newlineReader struct{ in *bufio.Reader }
type newlineWriter struct{ out io.Writer }

func (NewlineFramer) Reader(rw io.Reader) jsonrpc2.Reader {
return &newlineReader{in: bufio.NewReader(rw)}
}

func (f NewlineFramer) Writer(rw io.Writer) jsonrpc2.Writer {
return &newlineWriter{out: rw}
}

func (r *newlineReader) Read(ctx context.Context) (jsonrpc2.Message, int64, error) {
select {
case <-ctx.Done():
return nil, 0, ctx.Err()
default:
}
var total int64

// read a line
line, err := r.in.ReadBytes('\n')
total += int64(len(line))
if err != nil {
return nil, total, fmt.Errorf("failed reading line: %w", err)
}

msg, err := jsonrpc2.DecodeMessage(line[:total-1])
return msg, total, err
}

func (w *newlineWriter) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
}
data, err := jsonrpc2.EncodeMessage(msg)
if err != nil {
return 0, fmt.Errorf("marshaling message: %v", err)

Check failure on line 51 in cmd/remote/framer.go

View workflow job for this annotation

GitHub Actions / Build

non-wrapping format verb for fmt.Errorf. Use `%w` to format errors (errorlint)
}
n, err := w.out.Write(data)
total := int64(n)
if err == nil {
n, err = w.out.Write([]byte("\n"))
total += int64(n)
}
return total, err
}
155 changes: 155 additions & 0 deletions cmd/remote/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"context"
"crypto/tls"
"flag"
"log"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"

"github.com/enbility/eebus-go/api"
"github.com/enbility/eebus-go/usecases/cem/evcc"
"github.com/enbility/eebus-go/usecases/cem/evsecc"
eglpc "github.com/enbility/eebus-go/usecases/eg/lpc"
"github.com/enbility/eebus-go/usecases/ma/mpc"
shipapi "github.com/enbility/ship-go/api"
"github.com/enbility/ship-go/cert"
spineapi "github.com/enbility/spine-go/api"
"github.com/enbility/spine-go/model"
)

type eebusConfiguration struct {
vendorCode string
deviceBrand string
deviceModel string
serialNumber string
}

func loadCertificate(config eebusConfiguration, crtPath, keyPath string) tls.Certificate {
certificate, err := tls.LoadX509KeyPair(crtPath, keyPath)
if err != nil {
certificate, err = cert.CreateCertificate(config.vendorCode, config.deviceModel, "DE", config.serialNumber)
if err != nil {
log.Fatal(err)
}

if err = WriteKey(certificate, keyPath); err != nil {
log.Fatal(err)
}
if err = WriteCertificate(certificate, crtPath); err != nil {
log.Fatal(err)
}
}

return certificate
}

func main() {
config := eebusConfiguration{}

iface := flag.String("iface", "",
"Optional network interface the EEBUS connection should be limited to")
flag.StringVar(&config.vendorCode, "vendor", "", "EEBus vendor code")
flag.StringVar(&config.deviceBrand, "brand", "", "EEBus device brand")
flag.StringVar(&config.deviceModel, "model", "", "EEBus device model")
flag.StringVar(&config.serialNumber, "serial", "", "EEBus device serial")

flag.Parse()

if config.serialNumber == "" {
serialNumber, err := os.Hostname()
if err != nil {
log.Fatal(err)
}
config.serialNumber = serialNumber
}

if config.vendorCode == "" || config.deviceBrand == "" || config.deviceModel == "" {
flag.Usage()
return
}

certificate := loadCertificate(config, "cert.pem", "key.pem")

configuration, err := api.NewConfiguration(

Check failure on line 79 in cmd/remote/main.go

View workflow job for this annotation

GitHub Actions / Build

ineffectual assignment to err (ineffassign)
config.vendorCode, config.deviceBrand, config.deviceModel, config.serialNumber,
[]shipapi.DeviceCategoryType{
shipapi.DeviceCategoryTypeEnergyManagementSystem,
},
model.DeviceTypeTypeEnergyManagementSystem,
[]model.EntityTypeType{
model.EntityTypeTypeGridGuard,
model.EntityTypeTypeCEM,
},
23292, certificate, time.Second*4)
if *iface != "" {
configuration.SetInterfaces([]string{*iface})
log.Printf("waiting until %v is up", iface)
for {
ifi, err := net.InterfaceByName(*iface)
if err != nil {
log.Fatal(err)
}

// wait until interface is up and available for multicast
flags := net.FlagUp | net.FlagMulticast
if (ifi.Flags & flags) == flags {
break
}
time.Sleep(1 * time.Second)
}
log.Printf("interface online, continuing")
}

r, err := NewRemote(configuration)
if err != nil {
log.Fatal(err)
}

err = r.RegisterUseCase(model.EntityTypeTypeCEM, "EG-LPC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface {
return eglpc.NewLPC(localEntity, eventCB)
})
if err != nil {
log.Fatal(err)
}

err = r.RegisterUseCase(model.EntityTypeTypeCEM, "MA-MPC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface {
return mpc.NewMPC(localEntity, eventCB)
})
if err != nil {
log.Fatal(err)
}

err = r.RegisterUseCase(model.EntityTypeTypeCEM, "CEM-EVCC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface {
return evcc.NewEVCC(r.service, localEntity, eventCB)
})
if err != nil {
log.Fatal(err)
}

err = r.RegisterUseCase(model.EntityTypeTypeCEM, "CEM-EVSECC", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface {
return evsecc.NewEVSECC(localEntity, eventCB)
})
if err != nil {
log.Fatal(err)
}

ctx, cancelCtx := context.WithCancel(context.Background())
if err = r.Listen(ctx, "tcp", net.JoinHostPort("::", strconv.Itoa(3393))); err != nil {
log.Fatal(err)
}
log.Print("Started")

// Clean exit to make sure mdns shutdown is invoked
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
// User exit

cancelCtx()
}
Loading
Loading