diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index d649a4e0..fbf7b530 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -39,8 +39,8 @@ jobs: - name: Test run: go test -race -v -coverprofile=coverage_temp.out -covermode=atomic ./... - - name: Remove mocks and cmd from coverage - run: grep -v -e "/eebus-go/mocks/" -e "/eebus-go/usecases/mocks/" -e "/eebus-go/cmd/" coverage_temp.out > coverage.out + - name: Remove mocks and examples from coverage + run: grep -v -e "/eebus-go/mocks/" -e "/eebus-go/usecases/mocks/" -e "/eebus-go/examples/" coverage_temp.out > coverage.out - name: Send coverage uses: coverallsapp/github-action@v2 diff --git a/examples/ced/main.go b/examples/ced/main.go new file mode 100644 index 00000000..978b3271 --- /dev/null +++ b/examples/ced/main.go @@ -0,0 +1,307 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "os" + "os/signal" + "slices" + "strconv" + "syscall" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/service" + ucapi "github.com/enbility/eebus-go/usecases/api" + "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" +) + +var remoteSki string + +type controlbox struct { + myService *service.Service + + uclpc ucapi.EgLPCInterface + // uclpp ucapi.EgLPPInterface + ucmpc ucapi.MaMPCInterface + + isConnected bool +} + +func (h *controlbox) run() { + var err error + var certificate tls.Certificate + + if len(os.Args) == 5 { + remoteSki = os.Args[2] + + certificate, err = tls.LoadX509KeyPair(os.Args[3], os.Args[4]) + if err != nil { + usage() + log.Fatal(err) + } + } else { + certificate, err = cert.CreateCertificate("Demo", "Demo", "DE", "Demo-Unit-01") + if err != nil { + log.Fatal(err) + } + + pemdata := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Certificate[0], + }) + fmt.Println(string(pemdata)) + + b, err := x509.MarshalECPrivateKey(certificate.PrivateKey.(*ecdsa.PrivateKey)) + if err != nil { + log.Fatal(err) + } + pemdata = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + fmt.Println(string(pemdata)) + } + + port, err := strconv.Atoi(os.Args[1]) + if err != nil { + usage() + log.Fatal(err) + } + + configuration, err := api.NewConfiguration( + "Bosch", "eebus-go", "myHP", "12345678", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeHVAC}, + //JH model.DeviceTypeTypeElectricitySupplySystem, + model.DeviceTypeTypeGeneric, + // []model.EntityTypeType{model.EntityTypeTypeGridGuard}, + []model.EntityTypeType{model.EntityTypeTypeHeatPumpAppliance}, + port, certificate, time.Second*60) + if err != nil { + log.Fatal(err) + } + // configuration.SetAlternateIdentifier("Demo-ControlBox-123456789") + configuration.SetAlternateIdentifier("Bosch-myHP-12345678") + + h.myService = service.NewService(configuration, h) + h.myService.SetLogging(h) + + if err = h.myService.Setup(); err != nil { + fmt.Println(err) + return + } + + localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeGridGuard) + h.uclpc = lpc.NewLPC(localEntity, h.OnLPCEvent) + h.myService.AddUseCase(h.uclpc) + // h.uclpp = lpp.NewLPP(localEntity, h.OnLPPEvent) + // h.myService.AddUseCase(h.uclpp) + + h.ucmpc = mpc.NewMPC(localEntity, h.OnMPCEvent) + h.myService.AddUseCase(h.ucmpc) + + if len(remoteSki) == 0 { + os.Exit(0) + } + + h.myService.RegisterRemoteSKI(remoteSki) + + h.myService.Start() + // defer h.myService.Shutdown() +} + +// EEBUSServiceHandler + +func (h *controlbox) RemoteSKIConnected(service api.ServiceInterface, ski string) { + h.isConnected = true +} + +func (h *controlbox) RemoteSKIDisconnected(service api.ServiceInterface, ski string) { + h.isConnected = false +} + +func (h *controlbox) VisibleRemoteServicesUpdated(service api.ServiceInterface, entries []shipapi.RemoteService) { +} + +func (h *controlbox) ServiceShipIDUpdate(ski string, shipdID string) {} + +func (h *controlbox) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) { + if ski == remoteSki && detail.State() == shipapi.ConnectionStateRemoteDeniedTrust { + fmt.Println("The remote service denied trust. Exiting.") + h.myService.CancelPairingWithSKI(ski) + h.myService.UnregisterRemoteSKI(ski) + h.myService.Shutdown() + os.Exit(0) + } +} + +func (h *controlbox) AllowWaitingForTrust(ski string) bool { + return ski == remoteSki +} + +// LPC Event Handler + +func (h *controlbox) sendLimit(entity spineapi.EntityRemoteInterface) { + scenarios := h.uclpc.AvailableScenariosForEntity(entity) + if len(scenarios) == 0 || + !slices.Contains(scenarios, 1) { + return + } + + fmt.Println("Sending a limit in 5s...") + time.AfterFunc(time.Second*5, func() { + limit := ucapi.LoadLimit{ + Duration: time.Minute * 2, + IsActive: true, + Value: 7000, + } + + resultCB := func(msg model.ResultDataType) { + if *msg.ErrorNumber == model.ErrorNumberTypeNoError { + fmt.Println("Limit accepted.") + } else { + fmt.Println("Limit rejected. Code", *msg.ErrorNumber, "Description", *msg.Description) + } + } + msgCounter, err := h.uclpc.WriteConsumptionLimit(entity, limit, resultCB) + if err != nil { + fmt.Println("Failed to send limit", err) + return + } + fmt.Println("Sent limit to", entity.Device().Ski(), "with msgCounter", msgCounter) + }) +} +func (h *controlbox) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + if !h.isConnected { + return + } + + switch event { + case lpc.UseCaseSupportUpdate: + h.sendLimit(entity) + case lpc.DataUpdateLimit: + if currentLimit, err := h.uclpc.ConsumptionLimit(entity); err == nil { + fmt.Println("New Limit received", currentLimit.Value, "W") + } + default: + return + } +} + +func (h *controlbox) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + if !h.isConnected { + return + } + + switch event { + case lpc.UseCaseSupportUpdate: + h.sendLimit(entity) + case lpc.DataUpdateLimit: + if currentLimit, err := h.uclpc.ConsumptionLimit(entity); err == nil { + fmt.Println("New Limit received", currentLimit.Value, "W") + } + default: + return + } +} + +// JH experimental ---------------------------------------- +func (h *controlbox) OnMPCEvent(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + if !h.isConnected { + return + } + + switch event { + case mpc.DataUpdateEnergyConsumed: //.UseCaseSupportUpdate: + fmt.Println("EVENT: DataUpdateEnergyConsumed") + // TODO h.sendLimit(entity) + case mpc.DataUpdatePower: //UpdateLimit: + fmt.Println("EVENT: DataUpdatePower") + // if currentLimit, err := h.uclpc.ConsumptionLimit(entity); err == nil { + // fmt.Println("New Limit received", currentLimit.Value, "W") + // } + default: + return + } +} + +// JH experimental ---------------------------------------- + +// main app +func usage() { + fmt.Println("First Run:") + fmt.Println(" go run /examples/controlbox/main.go ") + fmt.Println() + fmt.Println("General Usage:") + fmt.Println(" go run /examples/controlbox/main.go ") +} + +func main() { + if len(os.Args) < 2 { + usage() + return + } + + h := controlbox{} + h.run() + + // 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 +} + +// Logging interface + +func (h *controlbox) Trace(args ...interface{}) { + // h.print("TRACE", args...) +} + +func (h *controlbox) Tracef(format string, args ...interface{}) { + // h.printFormat("TRACE", format, args...) +} + +func (h *controlbox) Debug(args ...interface{}) { + // h.print("DEBUG", args...) +} + +func (h *controlbox) Debugf(format string, args ...interface{}) { + // h.printFormat("DEBUG", format, args...) +} + +func (h *controlbox) Info(args ...interface{}) { + h.print("INFO ", args...) +} + +func (h *controlbox) Infof(format string, args ...interface{}) { + h.printFormat("INFO ", format, args...) +} + +func (h *controlbox) Error(args ...interface{}) { + h.print("ERROR", args...) +} + +func (h *controlbox) Errorf(format string, args ...interface{}) { + h.printFormat("ERROR", format, args...) +} + +func (h *controlbox) currentTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +func (h *controlbox) print(msgType string, args ...interface{}) { + value := fmt.Sprintln(args...) + fmt.Printf("%s %s %s", h.currentTimestamp(), msgType, value) +} + +func (h *controlbox) printFormat(msgType, format string, args ...interface{}) { + value := fmt.Sprintf(format, args...) + fmt.Println(h.currentTimestamp(), msgType, value) +} diff --git a/cmd/controlbox/main.go b/examples/controlbox/main.go similarity index 97% rename from cmd/controlbox/main.go rename to examples/controlbox/main.go index d09764cb..9965915d 100644 --- a/cmd/controlbox/main.go +++ b/examples/controlbox/main.go @@ -208,10 +208,10 @@ func (h *controlbox) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterfac // main app func usage() { fmt.Println("First Run:") - fmt.Println(" go run /cmd/controlbox/main.go ") + fmt.Println(" go run /examples/controlbox/main.go ") fmt.Println() fmt.Println("General Usage:") - fmt.Println(" go run /cmd/controlbox/main.go ") + fmt.Println(" go run /examples/controlbox/main.go ") } func main() { diff --git a/cmd/evse/main.go b/examples/evse/main.go similarity index 97% rename from cmd/evse/main.go rename to examples/evse/main.go index b75f1189..8dc17592 100644 --- a/cmd/evse/main.go +++ b/examples/evse/main.go @@ -170,10 +170,10 @@ func (h *evse) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent // main app func usage() { fmt.Println("First Run:") - fmt.Println(" go run /cmd/evse/main.go ") + fmt.Println(" go run /examples/evse/main.go ") fmt.Println() fmt.Println("General Usage:") - fmt.Println(" go run /cmd/evse/main.go ") + fmt.Println(" go run /examples/evse/main.go ") } func main() { diff --git a/examples/heatpump/main.go b/examples/heatpump/main.go new file mode 100644 index 00000000..59ebf660 --- /dev/null +++ b/examples/heatpump/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "log" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/enbility/eebus-go/api" + features "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +var remoteSki string + +type heatpump struct { + myService *service.Service +} + +func (h *heatpump) run() { + var err error + var certificate tls.Certificate + + if len(os.Args) == 5 { + remoteSki = os.Args[2] + + certificate, err = tls.LoadX509KeyPair(os.Args[3], os.Args[4]) + if err != nil { + usage() + log.Fatal(err) + } + } else { + certificate, err = cert.CreateCertificate("Demo", "Demo", "DE", "Demo-Unit-02") + if err != nil { + log.Fatal(err) + } + + pemdata := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Certificate[0], + }) + fmt.Println(string(pemdata)) + + b, err := x509.MarshalECPrivateKey(certificate.PrivateKey.(*ecdsa.PrivateKey)) + if err != nil { + log.Fatal(err) + } + pemdata = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) + fmt.Println(string(pemdata)) + } + + port, err := strconv.Atoi(os.Args[1]) + if err != nil { + usage() + log.Fatal(err) + } + + configuration, err := api.NewConfiguration( + "Demo", "Demo", "HeatPump", "234567890", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeHVAC}, + model.DeviceTypeTypeGeneric, + []model.EntityTypeType{model.EntityTypeTypeHeatPumpAppliance}, + port, certificate, time.Second*4) + if err != nil { + log.Fatal(err) + } + configuration.SetAlternateIdentifier("Demo-EVSE-234567890") + + h.myService = service.NewService(configuration, h) + h.myService.SetLogging(h) + + if err = h.myService.Setup(); err != nil { + fmt.Println(err) + return + } + + if len(remoteSki) == 0 { + os.Exit(0) + } + + h.AddFeatures() + _ = spine.Events.Subscribe(h) + + h.myService.RegisterRemoteSKI(remoteSki) + + h.myService.Start() + // defer h.myService.Shutdown() +} + +// EEBUSServiceHandler + +func (h *heatpump) RemoteSKIConnected(service api.ServiceInterface, ski string) {} + +func (h *heatpump) RemoteSKIDisconnected(service api.ServiceInterface, ski string) {} + +func (h *heatpump) VisibleRemoteServicesUpdated(service api.ServiceInterface, entries []shipapi.RemoteService) { +} + +func (h *heatpump) ServiceShipIDUpdate(ski string, shipdID string) {} + +func (h *heatpump) ServicePairingDetailUpdate(ski string, detail *shipapi.ConnectionStateDetail) { + if ski == remoteSki && detail.State() == shipapi.ConnectionStateRemoteDeniedTrust { + fmt.Println("The remote service denied trust. Exiting.") + h.myService.CancelPairingWithSKI(ski) + h.myService.UnregisterRemoteSKI(ski) + h.myService.Shutdown() + os.Exit(0) + } +} + +func (h *heatpump) AllowWaitingForTrust(ski string) bool { + return ski == remoteSki +} + +func (h *heatpump) AddFeatures() { + entityAddress := []model.AddressEntityType{1} + localEntity := spine.NewEntityLocal(h.myService.LocalDevice(), model.EntityTypeTypeCEM, entityAddress, time.Second*4) + h.myService.LocalDevice().AddEntity(localEntity) + + _ = localEntity.GetOrAddFeature(model.FeatureTypeTypeGeneric, model.RoleTypeServer) + _ = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceClassification, model.RoleTypeClient) +} + +func (h *heatpump) HandleResult(errorMsg spineapi.ResponseMessage) {} + +func (h *heatpump) HandleEvent(payload spineapi.EventPayload) { + switch payload.EventType { + case spineapi.EventTypeEntityChange: + entityType := payload.Entity.EntityType() + + switch payload.ChangeType { + case spineapi.ElementChangeAdd: + switch entityType { + case model.EntityTypeTypeGeneric: + h.deviceInformationConnected(payload.Entity) + h.hemsConnected(payload.Entity) + } + } + } +} + +func (h *heatpump) deviceInformationConnected(remoteEntity spineapi.EntityRemoteInterface) { + localDevice := h.myService.LocalDevice() + localEntity := localDevice.Entities()[1] + + nodeEntity := remoteEntity.Device().Entities()[0] + + deviceClassification, _ := features.NewDeviceClassification(localEntity, nodeEntity) + + if deviceClassification != nil { + if _, err := deviceClassification.RequestManufacturerDetails(); err != nil { + logging.Log().Debug(err) + } + } +} + +func (h *heatpump) hemsConnected(entity spineapi.EntityRemoteInterface) {} + +// main app +func usage() { + fmt.Println("First Run:") + fmt.Println(" go run /examples/heatpump/main.go ") + fmt.Println() + fmt.Println("General Usage:") + fmt.Println(" go run /examples/heatpump/main.go ") +} + +func main() { + if len(os.Args) < 2 { + usage() + return + } + + h := heatpump{} + h.run() + + // 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 +} + +// Logging interface + +func (h *heatpump) Trace(args ...interface{}) { + h.print("TRACE", args...) +} + +func (h *heatpump) Tracef(format string, args ...interface{}) { + h.printFormat("TRACE", format, args...) +} + +func (h *heatpump) Debug(args ...interface{}) { + h.print("DEBUG", args...) +} + +func (h *heatpump) Debugf(format string, args ...interface{}) { + h.printFormat("DEBUG", format, args...) +} + +func (h *heatpump) Info(args ...interface{}) { + h.print("INFO ", args...) +} + +func (h *heatpump) Infof(format string, args ...interface{}) { + h.printFormat("INFO ", format, args...) +} + +func (h *heatpump) Error(args ...interface{}) { + h.print("ERROR", args...) +} + +func (h *heatpump) Errorf(format string, args ...interface{}) { + h.printFormat("ERROR", format, args...) +} + +func (h *heatpump) currentTimestamp() string { + return time.Now().Format("2006-01-02 15:04:05") +} + +func (h *heatpump) print(msgType string, args ...interface{}) { + value := fmt.Sprintln(args...) + fmt.Printf("%s %s %s", h.currentTimestamp(), msgType, value) +} + +func (h *heatpump) printFormat(msgType, format string, args ...interface{}) { + value := fmt.Sprintf(format, args...) + fmt.Println(h.currentTimestamp(), msgType, value) +} diff --git a/cmd/hems/main.go b/examples/hems/main.go similarity index 98% rename from cmd/hems/main.go rename to examples/hems/main.go index 0a2a4d99..4c6e2a4f 100644 --- a/cmd/hems/main.go +++ b/examples/hems/main.go @@ -297,10 +297,10 @@ func (h *hems) HandleEVSEDeviceState(ski string, failure bool, errorCode string) // main app func usage() { fmt.Println("First Run:") - fmt.Println(" go run /cmd/hems/main.go ") + fmt.Println(" go run /examples/hems/main.go ") fmt.Println() fmt.Println("General Usage:") - fmt.Println(" go run /cmd/hems/main.go ") + fmt.Println(" go run /examples/hems/main.go ") } func main() { diff --git a/cmd/remote/framer.go b/examples/remote/framer.go similarity index 100% rename from cmd/remote/framer.go rename to examples/remote/framer.go diff --git a/cmd/remote/main.go b/examples/remote/main.go similarity index 100% rename from cmd/remote/main.go rename to examples/remote/main.go diff --git a/cmd/remote/reflection.go b/examples/remote/reflection.go similarity index 100% rename from cmd/remote/reflection.go rename to examples/remote/reflection.go diff --git a/cmd/remote/rpc.go b/examples/remote/rpc.go similarity index 100% rename from cmd/remote/rpc.go rename to examples/remote/rpc.go diff --git a/cmd/remote/ucs.go b/examples/remote/ucs.go similarity index 100% rename from cmd/remote/ucs.go rename to examples/remote/ucs.go diff --git a/cmd/remote/util.go b/examples/remote/util.go similarity index 100% rename from cmd/remote/util.go rename to examples/remote/util.go