├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── app └── hems.go ├── cert └── cert.go ├── cmd ├── client │ └── main.go └── server │ └── main.go ├── communication ├── connectioncontroller.go ├── connectioncontroller_device.go ├── connectioncontroller_export.go ├── connectioncontroller_heartbeat.go ├── connectioncontroller_spine.go ├── connectioncontroller_subscriptions.go ├── context.go └── sequencescontroller.go ├── device ├── const.go ├── entity │ ├── cem.go │ ├── deviceinformation.go │ └── helper.go └── feature │ ├── deviceclassification.go │ ├── deviceconfiguration.go │ ├── devicediagnosis.go │ ├── electricalconnection.go │ ├── helper.go │ ├── identification.go │ ├── incentivetable.go │ ├── loadcontrol.go │ ├── measurement.go │ ├── nodemaangement_destinationlist.go │ ├── nodemanagement.go │ ├── nodemanagement_defaileddiscovery.go │ ├── nodemanagement_subscription.go │ ├── nodemanagement_test.go │ ├── nodemanagement_usecase.go │ └── timeseries.go ├── go.mod ├── go.sum ├── mdns └── service.go ├── server ├── listener.go └── server.go ├── ship ├── client.go ├── client_data.go ├── connection.go ├── const.go ├── message │ ├── decode.go │ └── types.go ├── server.go ├── service.go ├── ship │ ├── format.go │ └── models.go ├── transport │ ├── accessmethods.go │ ├── close.go │ ├── data.go │ ├── handshake.go │ ├── hello.go │ ├── pin.go │ └── transport.go └── uniqueid.go ├── spine ├── context.go ├── device.go ├── entity.go ├── feature.go ├── model │ ├── bindingmanagement.go │ ├── commandframe.go │ ├── commondatatypes.go │ ├── commondatatypes_additions.go │ ├── datagram.go │ ├── deviceclassification.go │ ├── deviceclassification_test.go │ ├── deviceconfiguration.go │ ├── deviceconfiguration_test.go │ ├── devicediagnosis.go │ ├── devicediagnosis_test.go │ ├── electricalconnection.go │ ├── electricalconnection_test.go │ ├── identification.go │ ├── identification_test.go │ ├── incentivetable.go │ ├── loadcontrol.go │ ├── loadcontrol_test.go │ ├── measurement.go │ ├── measurement_test.go │ ├── models.go │ ├── models_test.go │ ├── networkmanagement.go │ ├── nodemanagement.go │ ├── nodemanagement_test.go │ ├── result.go │ ├── subscriptionmanagement.go │ ├── tarifinformation.go │ ├── timeseries.go │ ├── timetable.go │ ├── usecaseinformation.go │ ├── usecaseinformation_test.go │ └── version.go └── rw.go └── util ├── log.go └── marshal.go /.gitignore: -------------------------------------------------------------------------------- 1 | __debug_bin 2 | .vscode 3 | *.json 4 | *.log 5 | *.yaml 6 | linux-*.Dockerfile 7 | !modules/** 8 | dist 9 | *.py 10 | release 11 | out 12 | evcc.crt 13 | evcc.key 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2021 Andreas Linde (mail@andreaslinde.de) & andig (cpuidle@gmx.de) 3 | 4 | All rights reserved. 5 | 6 | The above copyright notice and this permission notice shall be included in all 7 | copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 11 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 12 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 13 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 14 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 15 | SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default clean install lint test assets build binaries test-release release 2 | 3 | TAG_NAME := $(shell git describe --abbrev=0 --tags) 4 | SHA := $(shell git rev-parse --short HEAD) 5 | VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA)) 6 | BUILD_DATE := $(shell date -u '+%Y-%m-%d_%H:%M:%S') 7 | 8 | IMAGE := andig/evcc 9 | ALPINE := 3.12 10 | TARGETS := arm.v6,arm.v8,amd64 11 | 12 | default: clean install assets lint test build 13 | 14 | clean: 15 | rm -rf dist/ 16 | 17 | install: 18 | go install github.com/mjibson/esc 19 | go install github.com/golang/mock/mockgen 20 | 21 | lint: 22 | golangci-lint run 23 | 24 | test: 25 | @echo "Running testsuite" 26 | go test ./... 27 | 28 | assets: 29 | @echo "Generating embedded assets" 30 | go generate ./... 31 | 32 | build: 33 | @echo Version: $(VERSION) $(BUILD_DATE) 34 | go build -v -tags=release -ldflags '-X "github.com/andig/evcc/server.Version=${VERSION}" -X "github.com/andig/evcc/server.Commit=${SHA}"' 35 | 36 | release-test: 37 | goreleaser --snapshot --skip-publish --rm-dist 38 | 39 | release: 40 | goreleaser --rm-dist 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EEBUS 2 | 3 | This is an open source implementation of parts of the [EEBUS protocol][1] Version 1.0.1 specification in [Go][2] 4 | 5 | **WARNING** The code in this repository is a mess and we know it! 6 | 7 | ## Current State 8 | 9 | - This should be considered as a proof of concept 10 | - The main goal (for now) was to get a working implementation for the EV use cases 11 | - The implementation does not yet have a clean and easy to understand architecture 12 | - The available documentation and missing reference implementation lead to the current state and will evolve from there 13 | - The main purpose of this implementation is being able to access EEBUS compatible chargers in [EVCC][3] 14 | - Only EV specific use cases are being worked on right now 15 | 16 | ## Missing 17 | 18 | - Adopt a proper code architecture 19 | - Proper error handling 20 | - Code cleanups 21 | - Increasing test coverage 22 | - Documentation 23 | - Proper APIs for public use 24 | 25 | ## Features 26 | 27 | - Partly implemented EEBUS Use Cases: 28 | - EVSE Commissioning and Configuration 29 | - EV Commissioning and Configuration 30 | - EV Charging Electricity Measurement 31 | - EV State of Charge 32 | - Optimization of Self-Consumption During EV Charging 33 | - Overload Protection by EV Charging Current Curtailment 34 | 35 | ## Background 36 | 37 | Having an EV with a bundled charger that basically only provides an EEBUS interface, the wish was to be able to control charging of the EV via PV. [EVCC][3] provided already a great foundation, API for different chargers, PV charging support, but had no way to connect with chargers via the EEBUS protocol. Even though the specification of EEBUS are open source, there seems to be no non commercial implementation existing. So [andig](https://github.com/andig/) and [Andreas Linde](https://github.com/DerAndereAndi) took up the challenge and see where we can get. 38 | 39 | [1]: https://www.eebus.org 40 | [2]: https://golang.org 41 | [3]: https://evcc.io 42 | -------------------------------------------------------------------------------- /app/hems.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/communication" 7 | "github.com/evcc-io/eebus/device/entity" 8 | "github.com/evcc-io/eebus/spine" 9 | "github.com/evcc-io/eebus/spine/model" 10 | ) 11 | 12 | func HEMS(details communication.ManufacturerDetails) spine.Device { 13 | localDeviceName := model.DeviceClassificationStringType(details.DeviceName) 14 | localDeviceCode := model.DeviceClassificationStringType(details.DeviceCode) 15 | localBrandName := model.DeviceClassificationStringType(details.BrandName) 16 | localDeviceAddress := model.AddressDeviceType(fmt.Sprintf("d:_i:%s", details.DeviceAddress)) 17 | 18 | manufacturerData := model.DeviceClassificationManufacturerDataType{ 19 | DeviceName: &localDeviceName, 20 | DeviceCode: &localDeviceCode, 21 | BrandName: &localBrandName, 22 | VendorName: &localBrandName, 23 | } 24 | 25 | operationState := model.DeviceDiagnosisOperatingStateType(model.DeviceDiagnosisOperatingStateEnumTypeNormalOperation) 26 | 27 | dev := &spine.DeviceImpl{ 28 | Address: localDeviceAddress, 29 | Type: model.DeviceTypeType(model.DeviceTypeEnumTypeEnergyManagementSystem), 30 | } 31 | 32 | eid := entity.Numerator([]uint{0}) 33 | 34 | { 35 | e := entity.DeviceInformation() 36 | e.SetAddress(eid()) 37 | dev.Add(e) 38 | } 39 | { 40 | e := entity.CEM() 41 | e.SetAddress(eid()) 42 | e.SetManufacturerData(manufacturerData) 43 | e.SetOperationState(operationState) 44 | dev.Add(e) 45 | } 46 | 47 | return dev 48 | } 49 | -------------------------------------------------------------------------------- /cert/cert.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/sha1" 10 | "crypto/tls" 11 | "crypto/x509" 12 | "crypto/x509/pkix" 13 | "encoding/pem" 14 | "errors" 15 | "fmt" 16 | "io/fs" 17 | "io/ioutil" 18 | "math/big" 19 | "net" 20 | "time" 21 | ) 22 | 23 | // publicKey returns public key of given certificate 24 | func publicKey(priv interface{}) interface{} { 25 | switch k := priv.(type) { 26 | case *rsa.PrivateKey: 27 | return &k.PublicKey 28 | case *ecdsa.PrivateKey: 29 | return &k.PublicKey 30 | default: 31 | return nil 32 | } 33 | } 34 | 35 | // CreateCertificate creates certificate for given subject and hosts 36 | func CreateCertificate(isCA bool, subject pkix.Name, hosts ...string) (tls.Certificate, error) { 37 | priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 38 | if err != nil { 39 | return tls.Certificate{}, err 40 | } 41 | 42 | // convert pubkey to ski 43 | pub, err := x509.MarshalECPrivateKey(priv) 44 | if err != nil { 45 | return tls.Certificate{}, err 46 | } 47 | ski := sha1.Sum(pub) 48 | 49 | template := x509.Certificate{ 50 | SerialNumber: big.NewInt(1), 51 | Subject: subject, 52 | SignatureAlgorithm: x509.ECDSAWithSHA256, 53 | SubjectKeyId: ski[:], 54 | NotBefore: time.Now(), 55 | NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10), 56 | BasicConstraintsValid: true, 57 | } 58 | 59 | for _, h := range hosts { 60 | if ip := net.ParseIP(h); ip != nil { 61 | template.IPAddresses = append(template.IPAddresses, ip) 62 | } else { 63 | template.DNSNames = append(template.DNSNames, h) 64 | } 65 | } 66 | 67 | if isCA { 68 | template.IsCA = true 69 | } 70 | 71 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) 72 | if err != nil { 73 | return tls.Certificate{}, err 74 | } 75 | 76 | tlsCert := tls.Certificate{ 77 | Certificate: [][]byte{derBytes}, 78 | PrivateKey: priv, 79 | } 80 | 81 | return tlsCert, nil 82 | } 83 | 84 | // pemBlockForKey marshals private key into pem block 85 | func pemBlockForKey(priv interface{}) (*pem.Block, error) { 86 | switch k := priv.(type) { 87 | case *rsa.PrivateKey: 88 | return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil 89 | case *ecdsa.PrivateKey: 90 | b, err := x509.MarshalECPrivateKey(k) 91 | if err != nil { 92 | return nil, fmt.Errorf("unable to marshal ECDSA private key: %w", err) 93 | } 94 | return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil 95 | default: 96 | return nil, errors.New("unknown private key type") 97 | } 98 | } 99 | 100 | // SaveX509KeyPair saves certificate to cert and key files 101 | func SaveX509KeyPair(certFile, keyFile string, cert tls.Certificate) error { 102 | out := &bytes.Buffer{} 103 | err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}) 104 | if err == nil { 105 | fmt.Println(out.String()) 106 | err = ioutil.WriteFile(certFile, out.Bytes(), fs.ModePerm) 107 | } 108 | 109 | if err == nil { 110 | var pb *pem.Block 111 | if pb, err = pemBlockForKey(cert.PrivateKey); err == nil { 112 | out.Reset() 113 | err = pem.Encode(out, pb) 114 | } 115 | } 116 | 117 | if err == nil { 118 | fmt.Println(out.String()) 119 | err = ioutil.WriteFile(keyFile, out.Bytes(), fs.ModePerm) 120 | } 121 | 122 | return err 123 | } 124 | 125 | // GetX509KeyPair saves returns the cert and key string values 126 | func GetX509KeyPair(cert tls.Certificate) (string, string, error) { 127 | var certValue, keyValue string 128 | 129 | out := &bytes.Buffer{} 130 | err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}) 131 | if err == nil { 132 | certValue = out.String() 133 | } 134 | 135 | if len(certValue) > 0 { 136 | var pb *pem.Block 137 | if pb, err = pemBlockForKey(cert.PrivateKey); err == nil { 138 | out.Reset() 139 | err = pem.Encode(out, pb) 140 | } 141 | } 142 | 143 | if err == nil { 144 | keyValue = out.String() 145 | } 146 | 147 | return certValue, keyValue, err 148 | } 149 | 150 | // SkiFromX509 extracts SKI from certificate 151 | func SkiFromX509(leaf *x509.Certificate) (string, error) { 152 | if len(leaf.SubjectKeyId) == 0 { 153 | return "", errors.New("missing SubjectKeyId") 154 | } 155 | return fmt.Sprintf("%0x", leaf.SubjectKeyId), nil 156 | } 157 | 158 | // SkiFromCert extracts SKI from certificate 159 | func SkiFromCert(cert tls.Certificate) (string, error) { 160 | leaf, err := x509.ParseCertificate(cert.Certificate[0]) 161 | if err != nil { 162 | return "", errors.New("failed parsing certificate: " + err.Error()) 163 | } 164 | return SkiFromX509(leaf) 165 | } 166 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509/pkix" 7 | "fmt" 8 | "log" 9 | "os/signal" 10 | "time" 11 | 12 | "os" 13 | 14 | "github.com/evcc-io/eebus/app" 15 | certhelper "github.com/evcc-io/eebus/cert" 16 | "github.com/evcc-io/eebus/communication" 17 | "github.com/evcc-io/eebus/mdns" 18 | "github.com/evcc-io/eebus/server" 19 | "github.com/evcc-io/eebus/ship" 20 | "github.com/evcc-io/eebus/spine/model" 21 | "github.com/libp2p/zeroconf/v2" 22 | ) 23 | 24 | const ( 25 | certFile = "evcc.crt" 26 | keyFile = "evcc.key" 27 | ) 28 | 29 | func certificate(details communication.ManufacturerDetails) tls.Certificate { 30 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 31 | if err != nil { 32 | if os.IsNotExist(err) { 33 | subject := pkix.Name{ 34 | CommonName: details.DeviceCode, 35 | Country: []string{"DE"}, 36 | Organization: []string{details.BrandName}, 37 | } 38 | 39 | if cert, err = certhelper.CreateCertificate(true, subject); err == nil { 40 | err = certhelper.SaveX509KeyPair(certFile, keyFile, cert) 41 | } 42 | } 43 | 44 | if err != nil { 45 | panic(err) 46 | } 47 | } 48 | 49 | return cert 50 | } 51 | 52 | func connectService(entry *zeroconf.ServiceEntry, id string, details communication.ManufacturerDetails, cert tls.Certificate) { 53 | svc, err := mdns.NewFromDNSEntry(entry) 54 | 55 | var conn ship.Conn 56 | if err == nil { 57 | log.Printf("%s: client connect", entry.HostName) 58 | conn, err = svc.Connect(log.Default(), id, cert, nil) 59 | } 60 | 61 | if err != nil { 62 | log.Printf("%s: client done: %v", entry.HostName, err) 63 | return 64 | } 65 | 66 | hems := app.HEMS(details) 67 | ctrl := communication.NewConnectionController(log.Default(), conn, hems) 68 | 69 | err = ctrl.Boot() 70 | if err != nil { 71 | log.Printf("%s: connection startup failed: ", err) 72 | return 73 | } 74 | } 75 | 76 | func discoverDNS(results <-chan *zeroconf.ServiceEntry, connector func(*zeroconf.ServiceEntry)) { 77 | for entry := range results { 78 | log.Println("mDNS:", entry.HostName, entry.AddrIPv4, entry.Text) 79 | 80 | connector(entry) 81 | } 82 | } 83 | 84 | func main() { 85 | details := communication.ManufacturerDetails{ 86 | BrandName: "EVCC", 87 | DeviceName: "EVCC", 88 | DeviceCode: "EVCC_HEMS_01", 89 | DeviceAddress: "EVCC_HEMS", 90 | } 91 | 92 | cert := certificate(details) 93 | 94 | id, err := ship.UniqueIDWithProtectedID(details.BrandName, "eebus") 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | srv := &server.Server{ 100 | Log: log.Default(), 101 | Addr: ":4712", 102 | Path: "/ship/", 103 | Certificate: cert, 104 | ID: id, 105 | Brand: details.BrandName, 106 | Model: details.DeviceCode, 107 | Type: string(model.DeviceTypeEnumTypeEnergyManagementSystem), 108 | Register: true, 109 | } 110 | 111 | // use announcements even in client example so remote party can see us 112 | zc, err := srv.Announce() 113 | if err != nil { 114 | panic(err) 115 | } 116 | defer zc.Shutdown() 117 | 118 | entries := make(chan *zeroconf.ServiceEntry) 119 | go discoverDNS(entries, func(entry *zeroconf.ServiceEntry) { 120 | connectService(entry, id, details, cert) 121 | }) 122 | 123 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 124 | defer cancel() 125 | 126 | // discover all services on the network (e.g. _workstation._tcp) 127 | if err = zeroconf.Browse(ctx, ship.ZeroconfType, ship.ZeroconfDomain, entries); err != nil { 128 | panic(fmt.Errorf("failed to browse: %w", err)) 129 | } 130 | 131 | ch := make(chan os.Signal, 1) 132 | signal.Notify(ch, os.Interrupt) 133 | go func() { 134 | for range ch { 135 | log.Println("mDNS: shutdown") 136 | zc.Shutdown() 137 | os.Exit(0) 138 | } 139 | }() 140 | 141 | select {} 142 | } 143 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509/pkix" 6 | "log" 7 | "os/signal" 8 | 9 | "os" 10 | 11 | "github.com/evcc-io/eebus/app" 12 | certhelper "github.com/evcc-io/eebus/cert" 13 | "github.com/evcc-io/eebus/communication" 14 | "github.com/evcc-io/eebus/server" 15 | "github.com/evcc-io/eebus/ship" 16 | "github.com/evcc-io/eebus/spine/model" 17 | "github.com/evcc-io/eebus/util" 18 | ) 19 | 20 | const ( 21 | certFile = "evcc.crt" 22 | keyFile = "evcc.key" 23 | ) 24 | 25 | func certificate(details communication.ManufacturerDetails) tls.Certificate { 26 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 27 | if err != nil { 28 | if os.IsNotExist(err) { 29 | subject := pkix.Name{ 30 | CommonName: details.DeviceCode, 31 | Country: []string{"DE"}, 32 | Organization: []string{details.BrandName}, 33 | } 34 | 35 | if cert, err = certhelper.CreateCertificate(true, subject); err == nil { 36 | err = certhelper.SaveX509KeyPair(certFile, keyFile, cert) 37 | } 38 | } 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | 45 | return cert 46 | } 47 | 48 | func main() { 49 | details := communication.ManufacturerDetails{ 50 | BrandName: "EVCC", 51 | DeviceName: "EVCC", 52 | DeviceCode: "EVCC_HEMS_01", 53 | DeviceAddress: "EVCC_HEMS", 54 | } 55 | 56 | cert := certificate(details) 57 | 58 | id, err := ship.UniqueIDWithProtectedID(details.BrandName, "eebus") 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | log := log.New(&util.LogWriter{Writer: os.Stdout, TimeFormat: "2006/01/02 15:04:05 "}, "[server] ", 0) 64 | 65 | srv := &server.Server{ 66 | Log: log, 67 | Addr: ":4712", 68 | Path: "/ship/", 69 | Certificate: cert, 70 | ID: id, 71 | Brand: details.BrandName, 72 | Model: details.DeviceCode, 73 | Type: string(model.DeviceTypeEnumTypeEnergyManagementSystem), 74 | Register: true, 75 | } 76 | 77 | zc, err := srv.Announce() 78 | if err != nil { 79 | panic(err) 80 | } 81 | defer zc.Shutdown() 82 | 83 | hems := app.HEMS(details) 84 | 85 | ln := &server.Listener{ 86 | Log: log, 87 | AccessMethod: id, 88 | Handler: func(ski string, conn ship.Conn) error { 89 | ctrl := communication.NewConnectionController(log, conn, hems) 90 | return ctrl.Boot() 91 | }, 92 | } 93 | 94 | go func() { 95 | err := srv.Listen(ln, nil) 96 | if err != nil { 97 | log.Println(err) 98 | } 99 | }() 100 | 101 | ch := make(chan os.Signal, 1) 102 | signal.Notify(ch, os.Interrupt) 103 | go func() { 104 | for range ch { 105 | log.Println("mDNS: shutdown") 106 | zc.Shutdown() 107 | os.Exit(0) 108 | } 109 | }() 110 | 111 | select {} 112 | } 113 | -------------------------------------------------------------------------------- /communication/connectioncontroller_device.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/evcc-io/eebus/spine" 8 | "github.com/evcc-io/eebus/spine/model" 9 | ) 10 | 11 | func (c *ConnectionController) context(datagram *model.DatagramType) spine.Context { 12 | ctrl := &contextImpl{ 13 | ConnectionController: c, 14 | } 15 | if datagram != nil { 16 | ctrl.datagram = *datagram 17 | } 18 | return ctrl 19 | } 20 | 21 | func (c *ConnectionController) GetDevice() spine.Device { 22 | return c.remoteDevice 23 | } 24 | 25 | func (c *ConnectionController) SetDevice(device spine.Device) { 26 | c.remoteDevice = device 27 | } 28 | 29 | func (c *ConnectionController) isEVConnected() bool { 30 | if c.remoteDevice == nil { 31 | return false 32 | } 33 | 34 | entity := c.remoteDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeEV)) 35 | 36 | return entity != nil 37 | } 38 | 39 | func (c *ConnectionController) UpdateDevice(stateChange model.NetworkManagementStateChangeType) { 40 | isEVConnected := c.isEVConnected() 41 | 42 | if stateChange == model.NetworkManagementStateChangeTypeAdded && isEVConnected { 43 | c.log.Println("detected ev connection") 44 | 45 | // a new EV is connected, so reset all data 46 | c.clientData.EVData = EVDataType{ 47 | ChargeState: EVChargeStateEnumTypeActive, 48 | Limits: make(map[uint]EVCurrentLimitType), 49 | } 50 | 51 | err := c.requestNodeManagementUseCaseData() 52 | if err != nil { 53 | c.log.Println("Sending UseCaseData read request failed!") 54 | } 55 | 56 | // TODO these actions should be usecase support specific! 57 | ctx := c.context(nil) 58 | err = c.sequencesController.StartSequenceFlow(ctx, SequenceEnumTypeEV) 59 | if err != nil { 60 | c.log.Println("error processing EV sequence") 61 | } 62 | 63 | le := c.localDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeCEM)) 64 | lf := le.FeatureByProps(model.FeatureTypeEnumTypeLoadControl, model.RoleTypeClient) 65 | 66 | re := c.remoteDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeEV)) 67 | rf := re.FeatureByProps(model.FeatureTypeEnumTypeLoadControl, model.RoleTypeServer) 68 | 69 | serverFeatureType := model.FeatureTypeType(model.FeatureTypeEnumTypeLoadControl) 70 | 71 | msgCounter, err := c.callNodeManagementBindingRequest(lf, rf, serverFeatureType) 72 | if err != nil { 73 | c.log.Println(msgCounter, err) 74 | } 75 | c.callDataUpdateHandler(EVDataElementUpdateEVConnectionState) 76 | } else if !isEVConnected && stateChange == model.NetworkManagementStateChangeTypeRemoved { 77 | c.log.Println("detected ev disconnection") 78 | c.clientData.EVData.ChargeState = EVChargeStateEnumTypeUnplugged 79 | c.remoteDevice.ResetUseCaseActors() 80 | 81 | // reset all the EV relevant features data 82 | for _, entity := range c.localDevice.GetEntities() { 83 | for _, feature := range entity.GetFeatures() { 84 | feature.EVDisconnect() 85 | } 86 | } 87 | 88 | c.callDataUpdateHandler(EVDataElementUpdateEVConnectionState) 89 | } 90 | } 91 | 92 | // TODO move this into NodeManagement feature implementation 93 | func (c *ConnectionController) requestNodeManagementDetailedDiscoveryData() error { 94 | cmdClassifier := model.CmdClassifierTypeRead 95 | nodeMgmtF, featureDestination := c.remoteNodeManagementFeature() 96 | 97 | datagram := model.DatagramType{ 98 | Header: model.HeaderType{ 99 | SpecificationVersion: &c.specificationVersion, 100 | AddressSource: spine.FeatureAddressType(nodeMgmtF), 101 | AddressDestination: &featureDestination, 102 | MsgCounter: c.msgCounter(), 103 | CmdClassifier: &cmdClassifier, 104 | }, 105 | Payload: model.PayloadType{ 106 | Cmd: []model.CmdType{{ 107 | NodeManagementDetailedDiscoveryData: &model.NodeManagementDetailedDiscoveryDataType{}, 108 | }}, 109 | }, 110 | } 111 | 112 | return c.sendSpineMessage(datagram) 113 | } 114 | 115 | func (c *ConnectionController) requestNodeManagementUseCaseData() error { 116 | cmdClassifier := model.CmdClassifierTypeRead 117 | nodeMgmtF, featureDestination := c.remoteNodeManagementFeature() 118 | 119 | datagram := model.DatagramType{ 120 | Header: model.HeaderType{ 121 | SpecificationVersion: &c.specificationVersion, 122 | AddressSource: spine.FeatureAddressType(nodeMgmtF), 123 | AddressDestination: &featureDestination, 124 | MsgCounter: c.msgCounter(), 125 | CmdClassifier: &cmdClassifier, 126 | }, 127 | Payload: model.PayloadType{ 128 | Cmd: []model.CmdType{{ 129 | NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{}, 130 | }}, 131 | }, 132 | } 133 | 134 | return c.sendSpineMessage(datagram) 135 | } 136 | 137 | func (c *ConnectionController) remoteNodeManagementFeature() (spine.Feature, model.FeatureAddressType) { 138 | deviceInfoE := c.localDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeDeviceInformation)) 139 | nodeMgmtF := deviceInfoE.FeatureByProps(model.FeatureTypeEnumTypeNodeManagement, model.RoleTypeSpecial) 140 | 141 | var feature0 model.AddressFeatureType = 0 142 | featureDestination := model.FeatureAddressType{ 143 | Entity: []model.AddressEntityType{0}, 144 | Feature: &feature0, 145 | } 146 | 147 | if c.remoteDevice != nil { 148 | featureDestination.Device = c.remoteDevice.Information().Description.DeviceAddress.Device 149 | } 150 | 151 | return nodeMgmtF, featureDestination 152 | } 153 | 154 | func (c *ConnectionController) callNodeManagementBindingRequest(lf, rf spine.Feature, featureType model.FeatureTypeType) (*model.MsgCounterType, error) { 155 | 156 | localDeviceInfoE := c.localDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeDeviceInformation)) 157 | localNodeMgmtF := localDeviceInfoE.FeatureByProps(model.FeatureTypeEnumTypeNodeManagement, model.RoleTypeSpecial) 158 | 159 | remoteDeviceInfoE := c.remoteDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeDeviceInformation)) 160 | remoteNodeMgmtF := remoteDeviceInfoE.FeatureByProps(model.FeatureTypeEnumTypeNodeManagement, model.RoleTypeSpecial) 161 | 162 | res := []model.CmdType{{ 163 | NodeManagementBindingRequestCall: &model.NodeManagementBindingRequestCallType{ 164 | BindingRequest: &model.BindingManagementRequestCallType{ 165 | ClientAddress: spine.FeatureAddressType(lf), 166 | ServerAddress: spine.FeatureAddressType(rf), 167 | ServerFeatureType: &featureType, 168 | }, 169 | }, 170 | }} 171 | 172 | ctrl := c.context(nil) 173 | 174 | return ctrl.Request(model.CmdClassifierTypeCall, *spine.FeatureAddressType(localNodeMgmtF), *spine.FeatureAddressType(remoteNodeMgmtF), true, res) 175 | } 176 | 177 | func (c *ConnectionController) featureAddressForTypeAndRole(device spine.Device, deviceLocation string, entityType model.EntityTypeEnumType, featureType model.FeatureTypeEnumType, role model.RoleType) (*model.FeatureAddressType, error) { 178 | entity := device.EntityByType(model.EntityTypeType(entityType)) 179 | if entity == nil { 180 | err := fmt.Errorf("couldn't find device with entity %s on %s device %v", entityType, deviceLocation, device) 181 | c.log.Println(err) 182 | return nil, err 183 | } 184 | 185 | feature := entity.FeatureByProps(featureType, role) 186 | if feature == nil { 187 | err := fmt.Errorf("couldn't find feature %s with role %s in entity %s on %s device %v", featureType, role, entityType, deviceLocation, device) 188 | return nil, err 189 | } 190 | 191 | address := spine.FeatureAddressType(feature) 192 | return address, nil 193 | } 194 | 195 | func (c *ConnectionController) featureTypeForAddress(entity spine.Entity, address *model.FeatureAddressType) (model.FeatureTypeEnumType, error) { 196 | for _, f := range entity.GetFeatures() { 197 | if reflect.DeepEqual(f.GetAddress(), address) { 198 | return f.GetType(), nil 199 | } 200 | } 201 | 202 | return "", fmt.Errorf("couldn't find feature with address %v", address) 203 | } 204 | -------------------------------------------------------------------------------- /communication/connectioncontroller_export.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type ManufacturerDetails struct { 10 | DeviceName string 11 | DeviceCode string 12 | DeviceAddress string 13 | BrandName string 14 | } 15 | 16 | type EVSEOperationStateEnumType string 17 | 18 | const ( 19 | EVSEOperationStateEnumTypeNormal = "normal" 20 | EVSEOperationStateEnumTypeFailure = "failure" 21 | ) 22 | 23 | type EVChargeStateEnumType string 24 | 25 | const ( 26 | EVChargeStateEnumTypeUnknown = "unknown" 27 | EVChargeStateEnumTypeUnplugged = "unplugged" 28 | EVChargeStateEnumTypeError = "error" 29 | EVChargeStateEnumTypePaused = "paused" 30 | EVChargeStateEnumTypeActive = "active" 31 | EVChargeStateEnumTypeFinished = "finished" 32 | ) 33 | 34 | type EVCommunicationStandardEnumType string 35 | 36 | const ( 37 | EVCommunicationStandardEnumTypeUnknown EVCommunicationStandardEnumType = "unknown" 38 | EVCommunicationStandardEnumTypeISO151182ED1 EVCommunicationStandardEnumType = "iso15118-2ed1" 39 | EVCommunicationStandardEnumTypeISO151182ED2 EVCommunicationStandardEnumType = "iso15118-2ed2" 40 | EVCommunicationStandardEnumTypeIEC61851 EVCommunicationStandardEnumType = "iec61851" 41 | ) 42 | 43 | type EVSEDataType struct { 44 | Manufacturer ManufacturerDetails 45 | OperationState EVSEOperationStateEnumType 46 | } 47 | 48 | type EVSEClientDataType struct { 49 | EVSEData EVSEDataType 50 | EVData EVDataType 51 | } 52 | 53 | type EVMeasurementsType struct { 54 | Timestamp time.Time 55 | Current sync.Map 56 | Power sync.Map 57 | ChargedEnergy float64 58 | SoC float64 59 | } 60 | 61 | type EVCurrentLimitType struct { 62 | Min, Max, Default float64 63 | } 64 | 65 | type EVPowerLimitType struct { 66 | Min, Max float64 67 | } 68 | 69 | type EVChargingStrategyEnumType string 70 | 71 | const ( 72 | EVChargingStrategyEnumTypeUnknown EVChargingStrategyEnumType = "unknown" 73 | EVChargingStrategyEnumTypeNoDemand EVChargingStrategyEnumType = "nodemand" 74 | EVChargingStrategyEnumTypeDirectCharging EVChargingStrategyEnumType = "directcharging" 75 | EVChargingStrategyEnumTypeTimedCharging EVChargingStrategyEnumType = "timedcharging" 76 | ) 77 | 78 | type EVDataType struct { 79 | UCSelfConsumptionAvailable bool 80 | UCCoordinatedChargingAvailable bool 81 | UCSoCAvailable bool 82 | AsymetricChargingSupported bool 83 | CommunicationStandard EVCommunicationStandardEnumType 84 | OverloadProtectionActive bool 85 | SelfConsumptionActive bool 86 | SoCDataAvailable bool 87 | ConnectedPhases uint 88 | ChargingStrategy EVChargingStrategyEnumType 89 | ChargingDemand float64 90 | ChargingTargetDuration time.Duration 91 | Manufacturer ManufacturerDetails 92 | Identification string 93 | ChargeState EVChargeStateEnumType 94 | Limits map[uint]EVCurrentLimitType 95 | LimitsPower EVPowerLimitType 96 | Measurements EVMeasurementsType 97 | } 98 | 99 | type EVDataElementUpdateType string 100 | 101 | const ( 102 | EVDataElementUpdateUseCaseSelfConsumption EVDataElementUpdateType = "usecaseselfconsumption" 103 | EVDataElementUpdateUseCaseSoC EVDataElementUpdateType = "usecasesoc" 104 | EVDataElementUpdateUseCaseCoordinatedCharging EVDataElementUpdateType = "usecasecoordinatedcharging" 105 | EVDataElementUpdateEVConnectionState EVDataElementUpdateType = "evconnectionstate" 106 | EVDataElementUpdateCommunicationStandard EVDataElementUpdateType = "communicationstandard" 107 | EVDataElementUpdateAsymetricChargingType EVDataElementUpdateType = "asymetricchargingtype" 108 | EVDataElementUpdateEVSEOperationState EVDataElementUpdateType = "evseoperationstate" 109 | EVDataElementUpdateEVChargeState EVDataElementUpdateType = "evchargestate" 110 | EVDataElementUpdateConnectedPhases EVDataElementUpdateType = "connectedphases" 111 | EVDataElementUpdatePowerLimits EVDataElementUpdateType = "powerlimits" 112 | EVDataElementUpdateAmperageLimits EVDataElementUpdateType = "amperagelimits" 113 | EVDataElementUpdateChargingStrategy EVDataElementUpdateType = "chargingstrategy" 114 | EVDataElementUpdateChargingPlanRequired EVDataElementUpdateType = "chargingplanrequired" 115 | ) 116 | 117 | type EVChargingSlot struct { 118 | Duration time.Duration 119 | MaxValue float64 // Watts 120 | Pricing float64 121 | } 122 | 123 | type EVChargingPlan struct { 124 | Duration time.Duration 125 | Slots []EVChargingSlot 126 | } 127 | 128 | func (c *ConnectionController) GetData() (*EVSEClientDataType, error) { 129 | if c == nil { 130 | return nil, errors.New("offline") 131 | } 132 | 133 | return c.clientData, nil 134 | } 135 | 136 | func (c *ConnectionController) SetDataUpdateHandler(dataUpdateHandler func(EVDataElementUpdateType, *EVSEClientDataType)) { 137 | c.dataUpdateHandler = dataUpdateHandler 138 | } 139 | 140 | func (c *ConnectionController) callDataUpdateHandler(updateType EVDataElementUpdateType) { 141 | if c.dataUpdateHandler != nil { 142 | c.dataUpdateHandler(updateType, c.clientData) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /communication/connectioncontroller_heartbeat.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "sync/atomic" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/evcc-io/eebus/spine/model" 11 | ) 12 | 13 | // TODO heartBeatCounter should be global on CEM level, not on connection level 14 | func (c *ConnectionController) heartBeatCounter() *uint64 { 15 | i := atomic.AddUint64(&c.heartBeatNum, 1) 16 | return &i 17 | } 18 | 19 | func (c *ConnectionController) sendHearbeat(stopC chan struct{}, d time.Duration) { 20 | ticker := time.NewTicker(d) 21 | for { 22 | select { 23 | case <-ticker.C: 24 | var heartBeatTimeout string = "PT4S" 25 | 26 | ctx := c.context(nil) 27 | 28 | var senderAddr, destinationAddr *model.FeatureAddressType 29 | localEntity := c.localDevice.EntityByType(model.EntityTypeType(model.EntityTypeEnumTypeCEM)) 30 | 31 | // we could have multiple subscriptions, e.g. if they are coming in for local client and server roles (which is wrong, but anyway) 32 | for _, item := range c.subscriptionEntries { 33 | // check if this is a subscription to a local devicediagnosis feature 34 | lfType, err := c.featureTypeForAddress(localEntity, item.ServerAddress) 35 | if err != nil { 36 | continue 37 | } 38 | 39 | if lfType != model.FeatureTypeEnumTypeDeviceDiagnosis { 40 | continue 41 | } 42 | 43 | senderAddr = item.ServerAddress 44 | destinationAddr = item.ClientAddress 45 | timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.9Z") 46 | 47 | res := []model.CmdType{{ 48 | DeviceDiagnosisHeartbeatData: &model.DeviceDiagnosisHeartbeatDataType{ 49 | Timestamp: ×tamp, 50 | HeartbeatCounter: c.heartBeatCounter(), 51 | HeartbeatTimeout: &heartBeatTimeout, 52 | }, 53 | }} 54 | 55 | // err := ctx.Notify(lf.GetAddress(), rf.GetAddress(), res) 56 | err = ctx.Notify(senderAddr, destinationAddr, res) 57 | if err != nil { 58 | c.log.Println("ERROR sending heartbeat: ", err) 59 | // TODO: when a connection is closed, we shouldn't get here 60 | return 61 | } 62 | } 63 | case <-stopC: 64 | return 65 | } 66 | } 67 | } 68 | 69 | func (c *ConnectionController) IsHeartbeatClosed() bool { 70 | select { 71 | case <-c.stopHeartbeatC: 72 | return true 73 | default: 74 | } 75 | 76 | return false 77 | } 78 | 79 | func (c *ConnectionController) stopHeartbeat() { 80 | c.stopMux.Lock() 81 | defer c.stopMux.Unlock() 82 | 83 | if c.stopHeartbeatC != nil && !c.IsHeartbeatClosed() { 84 | close(c.stopHeartbeatC) 85 | } 86 | } 87 | 88 | func (c *ConnectionController) startHeartBeatSend() { 89 | c.stopHeartbeatC = make(chan struct{}) 90 | 91 | go func() { 92 | c.sendHearbeat(c.stopHeartbeatC, 800*time.Millisecond) 93 | }() 94 | 95 | // catch signals 96 | go func() { 97 | signalC := make(chan os.Signal, 1) 98 | signal.Notify(signalC, os.Interrupt, syscall.SIGTERM) 99 | 100 | <-signalC // wait for signal 101 | c.stopHeartbeat() 102 | }() 103 | } 104 | -------------------------------------------------------------------------------- /communication/connectioncontroller_subscriptions.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "sync/atomic" 7 | 8 | "github.com/evcc-io/eebus/spine/model" 9 | ) 10 | 11 | func (c *ConnectionController) subscriptionId() *model.SubscriptionIdType { 12 | i := model.SubscriptionIdType(atomic.AddUint64(&c.subscriptionNum, 1)) 13 | return &i 14 | } 15 | 16 | func (c *ConnectionController) addSubscription(data model.SubscriptionManagementRequestCallType) error { 17 | var requestAllowed bool 18 | 19 | localAddress, localErr := c.featureAddressForTypeAndRole( 20 | c.localDevice, 21 | "local", 22 | model.EntityTypeEnumTypeDeviceInformation, 23 | model.FeatureTypeEnumTypeNodeManagement, 24 | model.RoleTypeSpecial, 25 | ) 26 | 27 | remoteAddress, remoteErr := c.featureAddressForTypeAndRole( 28 | c.remoteDevice, 29 | "remote", 30 | model.EntityTypeEnumTypeDeviceInformation, 31 | model.FeatureTypeEnumTypeNodeManagement, 32 | model.RoleTypeSpecial, 33 | ) 34 | 35 | // check if this is a subscription from nodemgmt to nodemgmt 36 | if localErr == nil && remoteErr == nil { 37 | if reflect.DeepEqual(data.ServerAddress, localAddress) && reflect.DeepEqual(data.ClientAddress, remoteAddress) { 38 | requestAllowed = true 39 | } 40 | } 41 | 42 | // check if this is a subscription from a client to a server of the same feature type 43 | if !requestAllowed { 44 | localAddress, localErr = c.featureAddressForTypeAndRole( 45 | c.localDevice, 46 | "local", 47 | model.EntityTypeEnumTypeCEM, 48 | model.FeatureTypeEnumType(*data.ServerFeatureType), 49 | model.RoleTypeServer, 50 | ) 51 | 52 | remoteAddress, remoteErr = c.featureAddressForTypeAndRole( 53 | c.remoteDevice, 54 | "remote", 55 | model.EntityTypeEnumType(c.remoteDevice.Entity(data.ClientAddress.Entity).GetType()), 56 | model.FeatureTypeEnumType(*data.ServerFeatureType), 57 | model.RoleTypeClient, 58 | ) 59 | 60 | // quick hack for ID. Charger which sends a subscription from featureType "Generic" to featureType "DeviceDiagnosis" 61 | altRemoteAddress, altRemoteErr := c.featureAddressForTypeAndRole( 62 | c.remoteDevice, 63 | "remote", 64 | model.EntityTypeEnumType(c.remoteDevice.Entity(data.ClientAddress.Entity).GetType()), 65 | model.FeatureTypeEnumTypeGeneric, 66 | model.RoleTypeClient, 67 | ) 68 | 69 | if localErr == nil && (remoteErr == nil || altRemoteErr == nil) { 70 | if reflect.DeepEqual(data.ServerAddress, localAddress) && (reflect.DeepEqual(data.ClientAddress, remoteAddress) || reflect.DeepEqual(data.ClientAddress, altRemoteAddress)) { 71 | requestAllowed = true 72 | } 73 | } 74 | } 75 | 76 | if !requestAllowed { 77 | msg := "subscription request not conforming a request from a client to a server of the same type" 78 | c.log.Println(msg) 79 | return errors.New(msg) 80 | } 81 | 82 | subscriptionEntry := model.SubscriptionManagementEntryDataType{ 83 | SubscriptionId: c.subscriptionId(), 84 | ClientAddress: data.ClientAddress, 85 | ServerAddress: data.ServerAddress, 86 | } 87 | 88 | c.subscriptionEntries = append(c.subscriptionEntries, subscriptionEntry) 89 | 90 | if model.FeatureTypeEnumType(*data.ServerFeatureType) == model.FeatureTypeEnumTypeDeviceDiagnosis { 91 | c.startHeartBeatSend() 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (c *ConnectionController) removeSubscription(data model.SubscriptionManagementDeleteCallType) error { 98 | // TODO: test this!!! 99 | 100 | var newSubscriptionEntries []model.SubscriptionManagementEntryDataType 101 | 102 | // according to the spec 7.4.4 103 | // a. The absence of "subscriptionDelete. clientAddress. device" SHALL be treated as if it was 104 | // present and set to the sender's "device" address part. 105 | // b. The absence of "subscriptionDelete. serverAddress. device" SHALL be treated as if it was 106 | // present and set to the recipient's "device" address part. 107 | 108 | clientAddress := data.ClientAddress 109 | if data.ClientAddress.Device == nil { 110 | clientAddress.Device = c.remoteDevice.Information().Description.DeviceAddress.Device 111 | } 112 | 113 | serverAddress := data.ServerAddress 114 | if data.ServerAddress.Device == nil { 115 | serverAddress.Device = c.localDevice.Information().Description.DeviceAddress.Device 116 | } 117 | 118 | for _, item := range c.subscriptionEntries { 119 | 120 | if reflect.DeepEqual(item.ClientAddress, clientAddress) { 121 | newSubscriptionEntries = append(newSubscriptionEntries, item) 122 | } 123 | } 124 | 125 | if len(newSubscriptionEntries) == len(c.subscriptionEntries) { 126 | return errors.New("could not find requested SubscriptionId to be removed") 127 | } 128 | 129 | c.subscriptionEntries = newSubscriptionEntries 130 | 131 | return nil 132 | } 133 | 134 | func (c *ConnectionController) subscriptions() []model.SubscriptionManagementEntryDataType { 135 | return c.subscriptionEntries 136 | } 137 | -------------------------------------------------------------------------------- /communication/context.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "github.com/evcc-io/eebus/spine" 5 | "github.com/evcc-io/eebus/spine/model" 6 | ) 7 | 8 | var _ spine.Context = (*contextImpl)(nil) 9 | 10 | // contextImpl provides request context to features for processing 11 | type contextImpl struct { 12 | *ConnectionController 13 | datagram model.DatagramType 14 | } 15 | 16 | func (c *contextImpl) CloseConnectionBecauseOfError(err error) { 17 | c.CloseConnection(err) 18 | } 19 | 20 | func (c *contextImpl) AddressSource() *model.FeatureAddressType { 21 | return c.datagram.Header.AddressSource 22 | } 23 | 24 | func (c *contextImpl) AddSubscription(data model.SubscriptionManagementRequestCallType) error { 25 | return c.addSubscription(data) 26 | } 27 | 28 | func (c *contextImpl) RemoveSubscription(data model.SubscriptionManagementDeleteCallType) error { 29 | return c.removeSubscription(data) 30 | } 31 | 32 | func (c *contextImpl) HeartbeatCounter() *uint64 { 33 | return c.heartBeatCounter() 34 | } 35 | 36 | func (c *contextImpl) Subscriptions() []model.SubscriptionManagementEntryDataType { 37 | return c.subscriptions() 38 | } 39 | 40 | // Send a subscription request to a remove server feature 41 | func (c *contextImpl) Subscribe(localFeature, remoteFeature spine.Feature, serverFeatureType model.FeatureTypeType) error { 42 | cmd := model.CmdType{ 43 | NodeManagementSubscriptionRequestCall: &model.NodeManagementSubscriptionRequestCallType{ 44 | SubscriptionRequest: &model.SubscriptionManagementRequestCallType{ 45 | ClientAddress: spine.FeatureAddressType(localFeature), 46 | ServerAddress: spine.FeatureAddressType(remoteFeature), 47 | ServerFeatureType: &serverFeatureType, 48 | }, 49 | }, 50 | } 51 | 52 | // we always send it to the remode NodeManagment feature, which always is at entity:[0],feature:0 53 | var feature0 model.AddressFeatureType = 0 54 | remoteAddress := model.FeatureAddressType{ 55 | Entity: []model.AddressEntityType{0}, 56 | Feature: &feature0, 57 | } 58 | remoteEntity := remoteFeature.GetEntity() 59 | if remoteEntity != nil { 60 | remoteDevice := remoteEntity.GetDevice() 61 | if remoteDevice != nil { 62 | deviceAddress := remoteDevice.GetAddress() 63 | remoteAddress.Device = &deviceAddress 64 | } 65 | } 66 | 67 | cmdClassifier := model.CmdClassifierTypeCall 68 | ackRequired := true 69 | 70 | datagram := model.DatagramType{ 71 | Header: model.HeaderType{ 72 | SpecificationVersion: &c.specificationVersion, 73 | AddressSource: spine.FeatureAddressType(localFeature), 74 | AddressDestination: &remoteAddress, 75 | MsgCounter: c.msgCounter(), 76 | CmdClassifier: &cmdClassifier, 77 | AckRequest: &ackRequired, 78 | }, 79 | Payload: model.PayloadType{ 80 | Cmd: []model.CmdType{cmd}, 81 | }, 82 | } 83 | 84 | return c.sendSpineMessage(datagram) 85 | } 86 | 87 | func (c *contextImpl) ProcessSequenceFlowRequest(featureType model.FeatureTypeEnumType, functionType model.FunctionEnumType, cmdClassifier model.CmdClassifierType) (*model.MsgCounterType, error) { 88 | return c.sendRequestToEVForFeatureAndFunction(featureType, functionType, cmdClassifier) 89 | } 90 | 91 | // Sends read request 92 | func (c *contextImpl) Request(cmdClassifier model.CmdClassifierType, senderAddress, destinationAddress model.FeatureAddressType, ackRequest bool, cmd []model.CmdType) (*model.MsgCounterType, error) { 93 | msgCounter := c.msgCounter() 94 | 95 | datagram := model.DatagramType{ 96 | Header: model.HeaderType{ 97 | SpecificationVersion: &c.specificationVersion, 98 | AddressSource: &senderAddress, 99 | AddressDestination: &destinationAddress, 100 | MsgCounter: msgCounter, 101 | CmdClassifier: &cmdClassifier, 102 | }, 103 | Payload: model.PayloadType{ 104 | Cmd: cmd, 105 | }, 106 | } 107 | 108 | if ackRequest { 109 | datagram.Header.AckRequest = &ackRequest 110 | } 111 | 112 | return msgCounter, c.sendSpineMessage(datagram) 113 | } 114 | 115 | // Reply sends reply to original sender 116 | func (c *contextImpl) Reply(cmdClassifier model.CmdClassifierType, cmd model.CmdType) error { 117 | // TODO where ack handling? 118 | 119 | // if ackRequest { 120 | // _ = c.sendAcknowledgementMessage(nil, featureSource, featureDestination, msgCounterReference) 121 | // } 122 | 123 | datagram := model.DatagramType{ 124 | Header: model.HeaderType{ 125 | SpecificationVersion: &c.specificationVersion, 126 | AddressSource: c.datagram.Header.AddressDestination, 127 | AddressDestination: c.datagram.Header.AddressSource, 128 | MsgCounter: c.msgCounter(), 129 | MsgCounterReference: c.datagram.Header.MsgCounter, 130 | CmdClassifier: &cmdClassifier, 131 | }, 132 | Payload: model.PayloadType{ 133 | Cmd: []model.CmdType{cmd}, 134 | }, 135 | } 136 | 137 | return c.sendSpineMessage(datagram) 138 | } 139 | 140 | // Notify sends notification to destination 141 | func (c *contextImpl) Notify(senderAddress, destinationAddress *model.FeatureAddressType, cmd []model.CmdType) error { 142 | cmdClassifier := model.CmdClassifierTypeNotify 143 | 144 | datagram := model.DatagramType{ 145 | Header: model.HeaderType{ 146 | SpecificationVersion: &c.specificationVersion, 147 | AddressSource: senderAddress, 148 | AddressDestination: destinationAddress, 149 | MsgCounter: c.msgCounter(), 150 | CmdClassifier: &cmdClassifier, 151 | }, 152 | Payload: model.PayloadType{ 153 | Cmd: cmd, 154 | }, 155 | } 156 | 157 | return c.sendSpineMessage(datagram) 158 | } 159 | 160 | // Write sends notification to destination 161 | func (c *contextImpl) Write(senderAddress, destinationAddress *model.FeatureAddressType, cmd []model.CmdType) error { 162 | cmdClassifier := model.CmdClassifierTypeWrite 163 | ackRequest := true 164 | 165 | datagram := model.DatagramType{ 166 | Header: model.HeaderType{ 167 | SpecificationVersion: &c.specificationVersion, 168 | AddressSource: senderAddress, 169 | AddressDestination: destinationAddress, 170 | MsgCounter: c.msgCounter(), 171 | CmdClassifier: &cmdClassifier, 172 | AckRequest: &ackRequest, 173 | }, 174 | Payload: model.PayloadType{ 175 | Cmd: cmd, 176 | }, 177 | } 178 | 179 | return c.sendSpineMessage(datagram) 180 | } 181 | -------------------------------------------------------------------------------- /communication/sequencescontroller.go: -------------------------------------------------------------------------------- 1 | package communication 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | "github.com/evcc-io/eebus/util" 9 | ) 10 | 11 | const ( 12 | SequenceEnumTypeEV = "EVSequence" 13 | ) 14 | 15 | type SequenceElement struct { 16 | featureType model.FeatureTypeEnumType 17 | functionType model.FunctionEnumType 18 | cmdClassifier model.CmdClassifierType 19 | msgCounter model.MsgCounterType 20 | } 21 | 22 | type SequenceFlow struct { 23 | currentId int 24 | sequenceType string 25 | elements []SequenceElement 26 | } 27 | 28 | type SequencesController struct { 29 | log util.Logger 30 | sequenceFlows []*SequenceFlow 31 | } 32 | 33 | func NewSequencesController(log util.Logger) *SequencesController { 34 | s := &SequencesController{ 35 | log: log, 36 | sequenceFlows: []*SequenceFlow{}, 37 | } 38 | 39 | return s 40 | } 41 | 42 | func (s *SequencesController) Boot() { 43 | s.sequenceFlows = append(s.sequenceFlows, s.setupEVConfigurationSequences()) 44 | } 45 | 46 | func (s *SequencesController) newElement(featureType model.FeatureTypeEnumType, functionType model.FunctionEnumType, cmdClassifier model.CmdClassifierType) SequenceElement { 47 | element := SequenceElement{ 48 | featureType: featureType, 49 | functionType: functionType, 50 | cmdClassifier: cmdClassifier, 51 | } 52 | return element 53 | } 54 | 55 | // sequenceEVCommissioningAndConfiguration 56 | func (s *SequencesController) setupEVConfigurationSequences() *SequenceFlow { 57 | newSequenceFlow := SequenceFlow{} 58 | newSequenceFlow.sequenceType = SequenceEnumTypeEV 59 | 60 | { 61 | element := s.newElement(model.FeatureTypeEnumTypeDeviceConfiguration, model.FunctionEnumTypeDeviceConfigurationKeyValueDescriptionListData, model.CmdClassifierTypeRead) 62 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 63 | } 64 | { 65 | element := s.newElement(model.FeatureTypeEnumTypeDeviceConfiguration, model.FunctionEnumTypeDeviceConfigurationKeyValueListData, model.CmdClassifierTypeRead) 66 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 67 | } 68 | 69 | // Identification only works with ISO, so wait until that is known 70 | { 71 | element := s.newElement(model.FeatureTypeEnumTypeIdentification, model.FunctionEnumTypeIdentificationListData, model.CmdClassifierTypeRead) 72 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 73 | } 74 | 75 | // get measurements only after the configuration is clear 76 | // this way we can make sure the limits are correct and don't provide intermediate values that could cause issues 77 | // e.g. providing IEC61851 limits even when the EV can use ISO15118, cause sending an IEC pause (0A) will fix the connection to IEC61851 78 | // or cause the EV to show a charging error 79 | 80 | { 81 | element := s.newElement(model.FeatureTypeEnumTypeMeasurement, model.FunctionEnumTypeMeasurementDescriptionListData, model.CmdClassifierTypeRead) 82 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 83 | } 84 | { 85 | element := s.newElement(model.FeatureTypeEnumTypeMeasurement, model.FunctionEnumTypeMeasurementListData, model.CmdClassifierTypeRead) 86 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 87 | } 88 | { 89 | element := s.newElement(model.FeatureTypeEnumTypeElectricalConnection, model.FunctionEnumTypeElectricalConnectionParameterDescriptionListData, model.CmdClassifierTypeRead) 90 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 91 | } 92 | { 93 | element := s.newElement(model.FeatureTypeEnumTypeElectricalConnection, model.FunctionEnumTypeElectricalConnectionDescriptionListData, model.CmdClassifierTypeRead) 94 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 95 | } 96 | { 97 | element := s.newElement(model.FeatureTypeEnumTypeElectricalConnection, model.FunctionEnumTypeElectricalConnectionPermittedValueSetListData, model.CmdClassifierTypeRead) 98 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 99 | } 100 | 101 | // LoadControl Limits are only useful once measurements and electrical connection data is available 102 | { 103 | element := s.newElement(model.FeatureTypeEnumTypeLoadControl, model.FunctionEnumTypeLoadControlLimitDescriptionListData, model.CmdClassifierTypeRead) 104 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 105 | } 106 | { 107 | element := s.newElement(model.FeatureTypeEnumTypeLoadControl, model.FunctionEnumTypeLoadControlLimitListData, model.CmdClassifierTypeRead) 108 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 109 | } 110 | 111 | // Coordinated Charging only works with ISO, so wait until that is known 112 | 113 | // Scenario 1 + 4 114 | { 115 | element := s.newElement(model.FeatureTypeEnumTypeTimeSeries, model.FunctionEnumTypeTimeSeriesDescriptionListData, model.CmdClassifierTypeRead) 116 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 117 | } 118 | { 119 | element := s.newElement(model.FeatureTypeEnumTypeTimeSeries, model.FunctionEnumTypeTimeSeriesListData, model.CmdClassifierTypeRead) 120 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 121 | } 122 | // Scenario 3 123 | { 124 | element := s.newElement(model.FeatureTypeEnumTypeIncentiveTable, model.FunctionEnumTypeIncentiveTableDescriptionData, model.CmdClassifierTypeRead) 125 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 126 | } 127 | { 128 | element := s.newElement(model.FeatureTypeEnumTypeIncentiveTable, model.FunctionEnumTypeIncentiveTableConstraintsData, model.CmdClassifierTypeRead) 129 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 130 | } 131 | { 132 | element := s.newElement(model.FeatureTypeEnumTypeIncentiveTable, model.FunctionEnumTypeIncentiveTableData, model.CmdClassifierTypeRead) 133 | newSequenceFlow.elements = append(newSequenceFlow.elements, element) 134 | } 135 | 136 | return &newSequenceFlow 137 | } 138 | 139 | func (s *SequencesController) processStepInSequenceFlow(ctx spine.Context, sequenceFlow *SequenceFlow) error { 140 | sequenceElement := sequenceFlow.elements[sequenceFlow.currentId] 141 | 142 | msgCounter, err := ctx.ProcessSequenceFlowRequest(sequenceElement.featureType, sequenceElement.functionType, sequenceElement.cmdClassifier) 143 | if err != nil { 144 | return err 145 | } 146 | if msgCounter == nil { 147 | return errors.New("No error returned with msgCounter as nil") 148 | } 149 | sequenceFlow.elements[sequenceFlow.currentId].msgCounter = *msgCounter 150 | 151 | return nil 152 | } 153 | 154 | // start a sequence of a specific type 155 | func (s *SequencesController) StartSequenceFlow(ctx spine.Context, sequenceType string) error { 156 | for _, sequenceFlow := range s.sequenceFlows { 157 | if sequenceFlow.sequenceType == sequenceType { 158 | return s.processStepInSequenceFlow(ctx, sequenceFlow) 159 | } 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // invoke the next step in a sequence 166 | func (s *SequencesController) ProcessResponseInSequences(ctx spine.Context, msgCounter *model.MsgCounterType) error { 167 | if msgCounter == nil { 168 | return errors.New("invalid msgCounter") 169 | } 170 | 171 | var sequenceFlow *SequenceFlow 172 | 173 | for _, flow := range s.sequenceFlows { 174 | currentSequenceElement := flow.elements[flow.currentId] 175 | if currentSequenceElement.msgCounter != 0 && currentSequenceElement.msgCounter == *msgCounter { 176 | sequenceFlow = flow 177 | } 178 | } 179 | 180 | if sequenceFlow != nil { 181 | // if sequenceFlow.sequenceType == SequenceEnumTypeEVMeasurement { 182 | sequenceFlow.currentId += 1 183 | if sequenceFlow.currentId < len(sequenceFlow.elements) { 184 | return s.processStepInSequenceFlow(ctx, sequenceFlow) 185 | } else { 186 | sequenceFlow.currentId = 0 187 | } 188 | // } 189 | } 190 | 191 | return nil 192 | } 193 | -------------------------------------------------------------------------------- /device/const.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | const SpecificationVersion = "1.1.1" 4 | -------------------------------------------------------------------------------- /device/entity/cem.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/evcc-io/eebus/device/feature" 5 | "github.com/evcc-io/eebus/spine" 6 | "github.com/evcc-io/eebus/spine/model" 7 | ) 8 | 9 | // Entities: 10 | // e[1] type=CEM, CEM Energy Guard 11 | // Features: 12 | // e[1] f-1 client.DeviceClassification - Device Classification 13 | // e[1] f-2 client.DeviceDiagnosis - Device Diagnosis 14 | // e[1] f-3 client.Measurement - Measurement for client 15 | // e[1] f-4 client.DeviceConfiguration - Device Configuration 16 | // e[1] f-5 server.DeviceDiagnosis - DeviceDiag 17 | // {RO} deviceDiagnosisStateData 18 | // {RO} deviceDiagnosisHeartbeatData 19 | // e[1] f-7 client.LoadControl - LoadControl client for CEM 20 | // e[1] f-8 client.Identification - EV identification 21 | // e[1] f-9 client.ElectricalConnection - Electrical Connection 22 | func CEM() spine.Entity { 23 | var entityType model.EntityTypeType = model.EntityTypeType(model.EntityTypeEnumTypeCEM) 24 | entity := &spine.EntityImpl{ 25 | Type: entityType, 26 | } 27 | 28 | fid := FeatureNumerator(1) 29 | { 30 | f := feature.NewDeviceClassificationClient() 31 | f.SetID(fid()) 32 | entity.Add(f) 33 | } 34 | { 35 | f := feature.NewDeviceDiagnosisClient() 36 | f.SetID(fid()) 37 | entity.Add(f) 38 | } 39 | { 40 | f := feature.NewMeasurementClient() 41 | f.SetID(fid()) 42 | entity.Add(f) 43 | } 44 | { 45 | f := feature.NewDeviceConfigurationClient() 46 | f.SetID(fid()) 47 | entity.Add(f) 48 | } 49 | { 50 | f := feature.NewDeviceDiagnosisServer() 51 | f.SetID(fid()) 52 | entity.Add(f) 53 | } 54 | { 55 | f := feature.NewLoadControlClient() 56 | f.SetID(fid()) 57 | entity.Add(f) 58 | } 59 | { 60 | f := feature.NewIdentificationClient() 61 | f.SetID(fid()) 62 | entity.Add(f) 63 | } 64 | { 65 | f := feature.NewElectricalConnectionClient() 66 | f.SetID(fid()) 67 | entity.Add(f) 68 | } 69 | { 70 | f := feature.NewTimeSeriesClient() 71 | f.SetID(fid()) 72 | entity.Add(f) 73 | } 74 | { 75 | f := feature.NewIncentiveTableClient() 76 | f.SetID(fid()) 77 | entity.Add(f) 78 | } 79 | 80 | return entity 81 | } 82 | -------------------------------------------------------------------------------- /device/entity/deviceinformation.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/evcc-io/eebus/device/feature" 5 | "github.com/evcc-io/eebus/spine" 6 | "github.com/evcc-io/eebus/spine/model" 7 | ) 8 | 9 | // Entities: 10 | // e[0] type=DeviceInformation 11 | // Features: 12 | // e[0] f-0 special.NodeManagement 13 | // {RO} nodeManagementDetailedDiscoveryData 14 | // {--} nodeManagementSubscriptionRequestCall 15 | // {--} nodeManagementBindingRequestCall 16 | // {--} nodeManagementSubscriptionDeleteCall 17 | // {--} nodeManagementBindingDeleteCall 18 | // {RO} nodeManagementSubscriptionData 19 | // {RO} nodeManagementBindingData 20 | // {RO} nodeManagementUseCaseData 21 | // e[0] f-1 server.DeviceClassification 22 | // {RO} deviceClassificationManufacturerData 23 | func DeviceInformation() spine.Entity { 24 | var entityType model.EntityTypeType = model.EntityTypeType(model.EntityTypeEnumTypeDeviceInformation) 25 | entity := &spine.EntityImpl{ 26 | Type: entityType, 27 | } 28 | 29 | fid := FeatureNumerator(0) 30 | 31 | { 32 | f := feature.NewNodeManagement() 33 | f.SetID(fid()) 34 | entity.Add(f) 35 | } 36 | { 37 | f := feature.NewDeviceClassificationServer() 38 | f.SetID(fid()) 39 | entity.Add(f) 40 | } 41 | 42 | return entity 43 | } 44 | -------------------------------------------------------------------------------- /device/entity/helper.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "github.com/evcc-io/eebus/spine/model" 4 | 5 | func Numerator(ids []uint) func() []model.AddressEntityType { 6 | id := ids[len(ids)-1] 7 | start := make([]model.AddressEntityType, len(ids)) 8 | for k, v := range ids { 9 | start[k] = model.AddressEntityType(v) 10 | } 11 | 12 | return func() []model.AddressEntityType { 13 | defer func() { 14 | id += 1 15 | }() 16 | 17 | addr := make([]model.AddressEntityType, len(start)) 18 | _ = copy(addr, start) 19 | addr[len(addr)-1] = model.AddressEntityType(id) 20 | 21 | return addr 22 | } 23 | } 24 | 25 | func FeatureNumerator(id uint) func() uint { 26 | return func() uint { 27 | defer func() { id += 1 }() 28 | return id 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /device/feature/deviceclassification.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | ) 9 | 10 | type DeviceClassificationDelegate interface { 11 | UpdateDeviceClassificationData(*DeviceClassification, model.FeatureAddressType, model.DeviceClassificationManufacturerDataType) 12 | } 13 | 14 | type DeviceClassification struct { 15 | *spine.FeatureImpl 16 | Delegate DeviceClassificationDelegate 17 | } 18 | 19 | func NewDeviceClassificationServer() spine.Feature { 20 | f := &DeviceClassification{ 21 | FeatureImpl: &spine.FeatureImpl{ 22 | Type: model.FeatureTypeEnumTypeDeviceClassification, 23 | Role: model.RoleTypeServer, 24 | }, 25 | } 26 | 27 | f.Add(model.FunctionEnumTypeDeviceClassificationManufacturerData, true, false) 28 | 29 | return f 30 | } 31 | 32 | func NewDeviceClassificationClient() spine.Feature { 33 | f := &DeviceClassification{ 34 | FeatureImpl: &spine.FeatureImpl{ 35 | Type: model.FeatureTypeEnumTypeDeviceClassification, 36 | Role: model.RoleTypeClient, 37 | }, 38 | } 39 | 40 | return f 41 | } 42 | 43 | func (f *DeviceClassification) requestManufacturerData(ctrl spine.Context, rf spine.Feature) (*model.MsgCounterType, error) { 44 | res := []model.CmdType{{ 45 | DeviceClassificationManufacturerData: &model.DeviceClassificationManufacturerDataType{}, 46 | }} 47 | 48 | return ctrl.Request(model.CmdClassifierTypeRead, *spine.FeatureAddressType(f), *spine.FeatureAddressType(rf), true, res) 49 | } 50 | 51 | func (f *DeviceClassification) readManufacturerData(ctrl spine.Context, data model.DeviceClassificationManufacturerDataType) error { 52 | manufacturerData := f.Entity.GetManufacturerData() 53 | 54 | res := model.CmdType{ 55 | DeviceClassificationManufacturerData: &manufacturerData, 56 | } 57 | 58 | err := ctrl.Reply(model.CmdClassifierTypeReply, res) 59 | 60 | return err 61 | } 62 | 63 | func (f *DeviceClassification) replyManufacturerData(ctrl spine.Context, rf model.FeatureAddressType, data model.DeviceClassificationManufacturerDataType) error { 64 | if f.Delegate != nil { 65 | f.Delegate.UpdateDeviceClassificationData(f, rf, data) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (f *DeviceClassification) HandleRequest(ctrl spine.Context, fct model.FunctionEnumType, op model.CmdClassifierType, rf spine.Feature) (*model.MsgCounterType, error) { 72 | switch fct { 73 | case model.FunctionEnumTypeDeviceClassificationManufacturerData: 74 | if op == model.CmdClassifierTypeRead { 75 | return f.requestManufacturerData(ctrl, rf) 76 | } 77 | return nil, fmt.Errorf("deviceclassification.handleRequest: FunctionEnumTypeDeviceClassificationManufacturerData op not implemented: %s", op) 78 | } 79 | 80 | return nil, fmt.Errorf("deviceclassification.handleRequest: FunctionEnumType not implemented: %s", fct) 81 | } 82 | 83 | func (f *DeviceClassification) Handle(ctrl spine.Context, rf model.FeatureAddressType, op model.CmdClassifierType, cmd model.CmdType, isPartialForCmd bool) error { 84 | switch { 85 | case cmd.DeviceClassificationManufacturerData != nil: 86 | data := cmd.DeviceClassificationManufacturerData 87 | switch op { 88 | case model.CmdClassifierTypeRead: 89 | return f.readManufacturerData(ctrl, *data) 90 | 91 | case model.CmdClassifierTypeReply: 92 | return f.replyManufacturerData(ctrl, rf, *data) 93 | 94 | default: 95 | return fmt.Errorf("deviceclassification.Handle: DeviceClassificationManufacturerData CmdClassifierType not implemented: %s", op) 96 | } 97 | 98 | case cmd.ResultData != nil: 99 | return f.HandleResultData(ctrl, op) 100 | 101 | default: 102 | return fmt.Errorf("deviceclassification.Handle: CmdType not implemented: %s", populatedFields(cmd)) 103 | } 104 | } 105 | 106 | func (f *DeviceClassification) ServerFound(ctrl spine.Context, rf spine.Feature) error { 107 | err := ctrl.Subscribe(f, rf, model.FeatureTypeType(f.Type)) 108 | if err != nil { 109 | return err 110 | } 111 | // this is a workaround so it will work for EVSE also right now 112 | _, err = f.requestManufacturerData(ctrl, rf) 113 | return err 114 | } 115 | -------------------------------------------------------------------------------- /device/feature/devicediagnosis.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | ) 9 | 10 | type DeviceDiagnosisDataType struct { 11 | OperationState model.DeviceDiagnosisOperatingStateEnumType 12 | } 13 | 14 | type DeviceDiagnosisData interface { 15 | GetDeviceDiagnosisDataData() DeviceDiagnosisDataType 16 | } 17 | 18 | type DeviceDiagnosisDelegate interface { 19 | UpdateDeviceDiagnosisData(*DeviceDiagnosis, model.FeatureAddressType, DeviceDiagnosisDataType) 20 | } 21 | 22 | type DeviceDiagnosis struct { 23 | *spine.FeatureImpl 24 | Delegate DeviceDiagnosisDelegate 25 | data DeviceDiagnosisDataType 26 | } 27 | 28 | func NewDeviceDiagnosisServer() spine.Feature { 29 | f := &DeviceDiagnosis{ 30 | FeatureImpl: &spine.FeatureImpl{ 31 | Type: model.FeatureTypeEnumTypeDeviceDiagnosis, 32 | Role: model.RoleTypeServer, 33 | }, 34 | } 35 | 36 | f.Add(model.FunctionEnumTypeDeviceDiagnosisStateData, true, false) 37 | f.Add(model.FunctionEnumTypeDeviceDiagnosisHeartbeatData, true, false) 38 | 39 | return f 40 | } 41 | 42 | func NewDeviceDiagnosisClient() spine.Feature { 43 | f := &DeviceDiagnosis{ 44 | FeatureImpl: &spine.FeatureImpl{ 45 | Type: model.FeatureTypeEnumTypeDeviceDiagnosis, 46 | Role: model.RoleTypeClient, 47 | }, 48 | } 49 | 50 | return f 51 | } 52 | 53 | func (f *DeviceDiagnosis) GetDeviceDiagnosisDataData() DeviceDiagnosisDataType { 54 | return f.data 55 | } 56 | 57 | func (f *DeviceDiagnosis) readHeartbeatData(ctrl spine.Context, data model.DeviceDiagnosisHeartbeatDataType) error { 58 | // TODO is this all we need here? 59 | 60 | var heartBeatTimeout string = "PT4S" 61 | 62 | res := model.CmdType{ 63 | DeviceDiagnosisHeartbeatData: &model.DeviceDiagnosisHeartbeatDataType{ 64 | HeartbeatCounter: ctrl.HeartbeatCounter(), 65 | HeartbeatTimeout: &heartBeatTimeout, 66 | }, 67 | } 68 | 69 | err := ctrl.Reply(model.CmdClassifierTypeReply, res) 70 | 71 | return err 72 | } 73 | 74 | func (f *DeviceDiagnosis) requestStateData(ctrl spine.Context, rf spine.Feature) error { 75 | res := []model.CmdType{{ 76 | DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{}, 77 | }} 78 | 79 | _, err := ctrl.Request(model.CmdClassifierTypeRead, *spine.FeatureAddressType(f), *spine.FeatureAddressType(rf), true, res) 80 | return err 81 | } 82 | 83 | func (f *DeviceDiagnosis) readStateData(ctrl spine.Context, data model.DeviceDiagnosisStateDataType) error { 84 | // TODO is this all we need here? 85 | 86 | operationState := f.Entity.GetOperationState() 87 | 88 | res := model.CmdType{ 89 | DeviceDiagnosisStateData: &model.DeviceDiagnosisStateDataType{ 90 | OperatingState: &operationState, 91 | }, 92 | } 93 | 94 | err := ctrl.Reply(model.CmdClassifierTypeReply, res) 95 | 96 | return err 97 | } 98 | 99 | func (f *DeviceDiagnosis) replyStateData(ctrl spine.Context, rf model.FeatureAddressType, data model.DeviceDiagnosisStateDataType) error { 100 | if f.Delegate != nil { 101 | f.Delegate.UpdateDeviceDiagnosisData(f, rf, DeviceDiagnosisDataType{OperationState: model.DeviceDiagnosisOperatingStateEnumType(*data.OperatingState)}) 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (f *DeviceDiagnosis) handleResultData(ctrl spine.Context, data model.ResultDataType) error { 108 | if *data.ErrorNumber != 0 { 109 | // close the connection as something is broken and right now hope this does not cause an indefinite loop 110 | err := fmt.Errorf("devicediagnosis.handleResultData: ErrorNumber %d", *data.ErrorNumber) 111 | if data.Description != nil { 112 | err = fmt.Errorf("devicediagnosis.handleResultData: %s", *data.Description) 113 | } 114 | ctrl.CloseConnectionBecauseOfError(err) 115 | 116 | return err 117 | } 118 | return nil 119 | } 120 | 121 | func (f *DeviceDiagnosis) Handle(ctrl spine.Context, rf model.FeatureAddressType, op model.CmdClassifierType, cmd model.CmdType, isPartialForCmd bool) error { 122 | switch { 123 | case cmd.DeviceDiagnosisHeartbeatData != nil: 124 | data := cmd.DeviceDiagnosisHeartbeatData 125 | switch op { 126 | case model.CmdClassifierTypeRead: 127 | return f.readHeartbeatData(ctrl, *data) 128 | 129 | default: 130 | return fmt.Errorf("devicediagnosis.Handle: DeviceDiagnosisHeartbeatData CmdClassifierType not implemented. %s", op) 131 | } 132 | 133 | case cmd.DeviceDiagnosisStateData != nil: 134 | data := cmd.DeviceDiagnosisStateData 135 | switch op { 136 | case model.CmdClassifierTypeRead: 137 | return f.readStateData(ctrl, *data) 138 | 139 | case model.CmdClassifierTypeReply: 140 | return f.replyStateData(ctrl, rf, *data) 141 | 142 | case model.CmdClassifierTypeNotify: 143 | return f.replyStateData(ctrl, rf, *data) 144 | 145 | default: 146 | return fmt.Errorf("devicediagnosis.Handle: DeviceDiagnosisStateData CmdClassifierType not implemented. %s", op) 147 | } 148 | 149 | case cmd.ResultData != nil: 150 | data := cmd.ResultData 151 | return f.handleResultData(ctrl, *data) 152 | 153 | default: 154 | return fmt.Errorf("devicediagnosis.Handle: CmdType not implemented. %s", populatedFields(cmd)) 155 | } 156 | } 157 | 158 | func (f *DeviceDiagnosis) ServerFound(ctrl spine.Context, rf spine.Feature) error { 159 | err := ctrl.Subscribe(f, rf, model.FeatureTypeType(f.Type)) 160 | if err != nil { 161 | return err 162 | } 163 | return f.requestStateData(ctrl, rf) 164 | } 165 | -------------------------------------------------------------------------------- /device/feature/helper.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import "reflect" 4 | 5 | // populatedFields finds the first non-nil field name for given struct 6 | func populatedFields(cmd interface{}) string { 7 | res := "Unknown" 8 | 9 | cmdFields := reflect.TypeOf(cmd) 10 | cmdValues := reflect.ValueOf(cmd) 11 | for i := 0; i < cmdFields.NumField(); i++ { 12 | if !cmdValues.Field(i).IsNil() { 13 | res = cmdFields.Field(i).Name 14 | break 15 | } 16 | } 17 | 18 | return res 19 | } 20 | -------------------------------------------------------------------------------- /device/feature/identification.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | ) 9 | 10 | type IdentificationDatasetDataType struct { 11 | IdentificationId uint 12 | IdentificationType model.IdentificationTypeEnumType 13 | IdentificationValue string 14 | } 15 | 16 | type IdentificationDelegate interface { 17 | UpdateIdentificationData(*Identification, []IdentificationDatasetDataType) 18 | } 19 | 20 | type Identification struct { 21 | *spine.FeatureImpl 22 | Delegate IdentificationDelegate 23 | datasetData []IdentificationDatasetDataType 24 | } 25 | 26 | func NewIdentificationClient() spine.Feature { 27 | f := &Identification{ 28 | FeatureImpl: &spine.FeatureImpl{ 29 | Type: model.FeatureTypeEnumTypeIdentification, 30 | Role: model.RoleTypeClient, 31 | }, 32 | } 33 | 34 | return f 35 | } 36 | 37 | func (f *Identification) EVDisconnectEvent() { 38 | f.datasetData = nil 39 | } 40 | 41 | func (f *Identification) requestListData(ctrl spine.Context, rf spine.Feature) (*model.MsgCounterType, error) { 42 | res := []model.CmdType{{ 43 | IdentificationListData: &model.IdentificationListDataType{}, 44 | }} 45 | 46 | return ctrl.Request(model.CmdClassifierTypeRead, *spine.FeatureAddressType(f), *spine.FeatureAddressType(rf), true, res) 47 | } 48 | 49 | func (f *Identification) replyListData(ctrl spine.Context, data model.IdentificationListDataType) error { 50 | // example data: 51 | // {"data":[{"header":[{"protocolId":"ee1.0"}]},{"payload":{"datagram":[{"header":[{"specificationVersion":"1.1.1"},{"addressSource":[{"device":"d:_i:19667_PorscheEVSE-00016544"},{"entity":[1,1]},{"feature":10}]},{"addressDestination":[{"device":"EVCC_HEMS"},{"entity":[1]},{"feature":7}]},{"msgCounter":21495},{"cmdClassifier":"notify"}]},{"payload":[{"cmd":[[{"identificationListData":[{"identificationData":[[{"identificationId":0},{"identificationType":"eui48"},{"identificationValue":"F0:7F:0C:07:9B:C7"}]]}]}]]}]}]}}]} 52 | 53 | f.datasetData = nil 54 | for _, item := range data.IdentificationData { 55 | if item.IdentificationId != nil || item.IdentificationType != nil || item.IdentificationValue != nil { 56 | continue 57 | } 58 | newItem := IdentificationDatasetDataType{ 59 | IdentificationId: uint(*item.IdentificationId), 60 | IdentificationType: model.IdentificationTypeEnumType(*item.IdentificationType), 61 | IdentificationValue: string(*item.IdentificationValue), 62 | } 63 | f.datasetData = append(f.datasetData, newItem) 64 | } 65 | 66 | if f.Delegate != nil { 67 | f.Delegate.UpdateIdentificationData(f, f.datasetData) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (f *Identification) HandleRequest(ctrl spine.Context, fct model.FunctionEnumType, op model.CmdClassifierType, rf spine.Feature) (*model.MsgCounterType, error) { 74 | switch fct { 75 | case model.FunctionEnumTypeIdentificationListData: 76 | if op == model.CmdClassifierTypeRead { 77 | return f.requestListData(ctrl, rf) 78 | } 79 | return nil, fmt.Errorf("identification.handleRequest: FunctionEnumTypeIdentificationListData op not implemented: %s", op) 80 | } 81 | 82 | return nil, fmt.Errorf("identification.handleRequest: FunctionEnumType not implemented: %s", fct) 83 | } 84 | 85 | func (f *Identification) Handle(ctrl spine.Context, rf model.FeatureAddressType, op model.CmdClassifierType, cmd model.CmdType, isPartialForCmd bool) error { 86 | switch { 87 | case cmd.IdentificationListData != nil: 88 | data := cmd.IdentificationListData 89 | switch op { 90 | case model.CmdClassifierTypeReply: 91 | return f.replyListData(ctrl, *data) 92 | 93 | case model.CmdClassifierTypeNotify: 94 | return f.replyListData(ctrl, *data) 95 | 96 | default: 97 | return fmt.Errorf("identification.Handle: IdentificationListData CmdClassifierType not implemented: %s", op) 98 | } 99 | case cmd.ResultData != nil: 100 | return f.HandleResultData(ctrl, op) 101 | 102 | default: 103 | return fmt.Errorf("identification.Handle: CmdType not implemented: %s", populatedFields(cmd)) 104 | } 105 | } 106 | 107 | func (f *Identification) ServerFound(ctrl spine.Context, rf spine.Feature) error { 108 | return ctrl.Subscribe(f, rf, model.FeatureTypeType(f.Type)) 109 | } 110 | -------------------------------------------------------------------------------- /device/feature/nodemaangement_destinationlist.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | ) 9 | 10 | func (f *NodeManagement) readDestinationListData(ctrl spine.Context, data model.NodeManagementDestinationListDataType, isPartialForCmd bool) error { 11 | localDevice := f.Entity.GetDevice() 12 | 13 | deviceAddress := localDevice.GetAddress() 14 | deviceType := localDevice.GetType() 15 | featureSet := model.NetworkManagementFeatureSetTypeSimple 16 | 17 | res := model.CmdType{ 18 | NodeManagementDestinationListData: &model.NodeManagementDestinationListDataType{ 19 | NodeManagementDestinationData: []model.NodeManagementDestinationDataType{ 20 | { 21 | DeviceDescription: &model.NetworkManagementDeviceDescriptionDataType{ 22 | DeviceAddress: &model.DeviceAddressType{ 23 | Device: &deviceAddress, 24 | }, 25 | DeviceType: &deviceType, 26 | NetworkFeatureSet: &featureSet, 27 | }, 28 | }, 29 | }, 30 | }, 31 | } 32 | 33 | return ctrl.Reply(model.CmdClassifierTypeReply, res) 34 | } 35 | 36 | func (f *NodeManagement) handleNodeManagementDestinationListData(ctrl spine.Context, op model.CmdClassifierType, data *model.NodeManagementDestinationListDataType, isPartialForCmd bool) error { 37 | switch op { 38 | case model.CmdClassifierTypeRead: 39 | return f.readDestinationListData(ctrl, *data, isPartialForCmd) 40 | default: 41 | return fmt.Errorf("nodemanagement.handleNodeManagementDestinationListData: NodeManagementDestinationListDataType CmdClassifierType not implemented: %s", op) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /device/feature/nodemanagement.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | ) 9 | 10 | type NodeManagementDelegate interface { 11 | UpdateUseCaseSupportData(*NodeManagement, model.UseCaseNameType, bool) 12 | } 13 | 14 | type NodeManagement struct { 15 | *spine.FeatureImpl 16 | Delegate NodeManagementDelegate 17 | } 18 | 19 | func NewNodeManagement() spine.Feature { 20 | f := &NodeManagement{ 21 | FeatureImpl: &spine.FeatureImpl{ 22 | Type: model.FeatureTypeEnumTypeNodeManagement, 23 | Role: model.RoleTypeSpecial, 24 | }, 25 | } 26 | 27 | f.Add(model.FunctionEnumTypeNodeManagementDetailedDiscoveryData, true, false) 28 | f.Add(model.FunctionEnumTypeNodeManagementSubscriptionRequestCall, false, false) 29 | f.Add(model.FunctionEnumTypeNodeManagementBindingRequestCall, false, false) 30 | f.Add(model.FunctionEnumTypeNodeManagementSubscriptionDeleteCall, false, false) 31 | f.Add(model.FunctionEnumTypeNodeManagementBindingDeleteCall, false, false) 32 | f.Add(model.FunctionEnumTypeNodeManagementSubscriptionData, true, false) 33 | f.Add(model.FunctionEnumTypeNodeManagementBindingData, true, false) 34 | f.Add(model.FunctionEnumTypeNodeManagementUseCaseData, true, false) 35 | 36 | return f 37 | } 38 | 39 | func (f *NodeManagement) Handle(ctrl spine.Context, rf model.FeatureAddressType, op model.CmdClassifierType, cmd model.CmdType, isPartialForCmd bool) error { 40 | switch { 41 | case cmd.NodeManagementDestinationListData != nil: 42 | return f.handleNodeManagementDestinationListData(ctrl, op, cmd.NodeManagementDestinationListData, isPartialForCmd) 43 | 44 | case cmd.NodeManagementDetailedDiscoveryData != nil: 45 | return f.handleDetailedDiscoveryData(ctrl, op, cmd.NodeManagementDetailedDiscoveryData, isPartialForCmd) 46 | 47 | case cmd.NodeManagementSubscriptionRequestCall != nil: 48 | return f.handleSubscriptionRequestCall(ctrl, op, cmd.NodeManagementSubscriptionRequestCall, isPartialForCmd) 49 | 50 | case cmd.NodeManagementSubscriptionDeleteCall != nil: 51 | return f.handleSubscriptionDeleteCall(ctrl, op, cmd.NodeManagementSubscriptionDeleteCall, isPartialForCmd) 52 | 53 | case cmd.NodeManagementSubscriptionData != nil: 54 | return f.handleSubscriptionData(ctrl, op, cmd.NodeManagementSubscriptionData, isPartialForCmd) 55 | 56 | case cmd.NodeManagementUseCaseData != nil: 57 | return f.handleUseCaseData(ctrl, op, cmd.NodeManagementUseCaseData, isPartialForCmd) 58 | 59 | case cmd.ResultData != nil: 60 | return f.HandleResultData(ctrl, op) 61 | 62 | default: 63 | return fmt.Errorf("nodemanagement.Handle: CmdType not implemented: %s", populatedFields(cmd)) 64 | } 65 | } 66 | 67 | func (f *NodeManagement) ServerFound(ctrl spine.Context, rf spine.Feature) error { 68 | return ctrl.Subscribe(f, rf, model.FeatureTypeType(f.Type)) 69 | } 70 | -------------------------------------------------------------------------------- /device/feature/nodemanagement_subscription.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/evcc-io/eebus/spine" 7 | "github.com/evcc-io/eebus/spine/model" 8 | ) 9 | 10 | // route subscription request calls to the appropriate feature implementation and add the subscription to the current list 11 | func (f *NodeManagement) replySubscriptionData(ctrl spine.Context) error { 12 | res := model.CmdType{ 13 | NodeManagementSubscriptionData: &model.NodeManagementSubscriptionDataType{ 14 | SubscriptionEntry: ctrl.Subscriptions(), 15 | }, 16 | } 17 | 18 | return ctrl.Reply(model.CmdClassifierTypeReply, res) 19 | } 20 | 21 | func (f *NodeManagement) handleSubscriptionData(ctrl spine.Context, op model.CmdClassifierType, data *model.NodeManagementSubscriptionDataType, isPartialForCmd bool) error { 22 | switch op { 23 | case model.CmdClassifierTypeCall: 24 | return f.replySubscriptionData(ctrl) 25 | 26 | default: 27 | return fmt.Errorf("nodemanagement.handleSubscriptionDeleteCall: NodeManagementSubscriptionRequestCall CmdClassifierType not implemented: %s", op) 28 | } 29 | } 30 | 31 | func (f *NodeManagement) handleSubscriptionRequestCall(ctrl spine.Context, op model.CmdClassifierType, data *model.NodeManagementSubscriptionRequestCallType, isPartialForCmd bool) error { 32 | switch op { 33 | case model.CmdClassifierTypeCall: 34 | return ctrl.AddSubscription(*data.SubscriptionRequest) 35 | // in case of subscription failure, should we send an resulterror reply? 36 | 37 | default: 38 | return fmt.Errorf("nodemanagement.handleSubscriptionRequestCall: NodeManagementSubscriptionRequestCall CmdClassifierType not implemented: %s", op) 39 | } 40 | } 41 | 42 | func (f *NodeManagement) handleSubscriptionDeleteCall(ctrl spine.Context, op model.CmdClassifierType, data *model.NodeManagementSubscriptionDeleteCallType, isPartialForCmd bool) error { 43 | switch op { 44 | case model.CmdClassifierTypeCall: 45 | return ctrl.RemoveSubscription(*data.SubscriptionDelete) 46 | 47 | default: 48 | return fmt.Errorf("nodemanagement.handleSubscriptionDeleteCall: NodeManagementSubscriptionRequestCall CmdClassifierType not implemented: %s", op) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /device/feature/nodemanagement_usecase.go: -------------------------------------------------------------------------------- 1 | package feature 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/evcc-io/eebus/spine" 8 | "github.com/evcc-io/eebus/spine/model" 9 | ) 10 | 11 | func (f *NodeManagement) readUseCaseData(ctrl spine.Context, data model.NodeManagementUseCaseDataType) error { 12 | // TODO: generate this! 13 | 14 | ucTrue := true 15 | 16 | deviceAddress := f.GetEntity().GetDevice().GetAddress() 17 | actor := model.UseCaseActorType("CEM") 18 | var useCaseSupport []model.UseCaseSupportType 19 | useCaseVersion := model.SpecificationVersionType("1.0.1") 20 | 21 | { 22 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeEVSECommissioningAndConfiguration) 23 | useCaseItem := model.UseCaseSupportType{ 24 | UseCaseVersion: &useCaseVersion, 25 | UseCaseName: &useCaseName, 26 | ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2}, 27 | UseCaseAvailable: &ucTrue, 28 | } 29 | useCaseSupport = append(useCaseSupport, useCaseItem) 30 | } 31 | { 32 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeEVCommissioningAndConfiguration) 33 | useCaseItem := model.UseCaseSupportType{ 34 | UseCaseVersion: &useCaseVersion, 35 | UseCaseName: &useCaseName, 36 | ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, 37 | UseCaseAvailable: &ucTrue, 38 | } 39 | useCaseSupport = append(useCaseSupport, useCaseItem) 40 | } 41 | // { 42 | // useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeEVChargingSummary) 43 | // useCaseItem := model.UseCaseSupportType{ 44 | // UseCaseVersion: &useCaseVersion, 45 | // UseCaseName: &useCaseName, 46 | // ScenarioSupport: []model.UseCaseScenarioSupportType{1}, 47 | // } 48 | // useCaseSupport = append(useCaseSupport, useCaseItem) 49 | // } 50 | { 51 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeMeasurementOfElectricityDuringEVCharging) 52 | useCaseItem := model.UseCaseSupportType{ 53 | UseCaseVersion: &useCaseVersion, 54 | UseCaseName: &useCaseName, 55 | ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, 56 | UseCaseAvailable: &ucTrue, 57 | } 58 | useCaseSupport = append(useCaseSupport, useCaseItem) 59 | } 60 | { 61 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeCoordinatedEVCharging) 62 | useCaseItem := model.UseCaseSupportType{ 63 | UseCaseVersion: &useCaseVersion, 64 | UseCaseName: &useCaseName, 65 | ScenarioSupport: []model.UseCaseScenarioSupportType{1, 3, 4, 5, 6, 7, 8}, 66 | // ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, 67 | UseCaseAvailable: &ucTrue, 68 | } 69 | useCaseSupport = append(useCaseSupport, useCaseItem) 70 | } 71 | { 72 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeOptimizationOfSelfConsumptionDuringEVCharging) 73 | useCaseItem := model.UseCaseSupportType{ 74 | UseCaseVersion: &useCaseVersion, 75 | UseCaseName: &useCaseName, 76 | ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, 77 | UseCaseAvailable: &ucTrue, 78 | } 79 | useCaseSupport = append(useCaseSupport, useCaseItem) 80 | } 81 | { 82 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeEVStateOfCharge) 83 | useCaseItem := model.UseCaseSupportType{ 84 | UseCaseVersion: &useCaseVersion, 85 | UseCaseName: &useCaseName, 86 | ScenarioSupport: []model.UseCaseScenarioSupportType{1}, 87 | UseCaseAvailable: &ucTrue, 88 | } 89 | useCaseSupport = append(useCaseSupport, useCaseItem) 90 | } 91 | { 92 | useCaseName := model.UseCaseNameType(model.UseCaseNameEnumTypeOverloadProtectionByEVChargingCurrentCurtailment) 93 | useCaseItem := model.UseCaseSupportType{ 94 | UseCaseVersion: &useCaseVersion, 95 | UseCaseName: &useCaseName, 96 | ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, 97 | UseCaseAvailable: &ucTrue, 98 | } 99 | useCaseSupport = append(useCaseSupport, useCaseItem) 100 | } 101 | 102 | res := model.CmdType{ 103 | NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ 104 | UseCaseInformation: []model.UseCaseInformationDataType{ 105 | { 106 | Address: &model.FeatureAddressType{Device: &deviceAddress}, 107 | Actor: &actor, 108 | UseCaseSupport: useCaseSupport, 109 | }, 110 | }, 111 | }, 112 | } 113 | 114 | return ctrl.Reply(model.CmdClassifierTypeReply, res) 115 | } 116 | 117 | func (f *NodeManagement) updateSupportedUseCases(ctrl spine.Context, remoteDevice spine.Device, data model.NodeManagementUseCaseDataType) error { 118 | useCaseInformation := data.UseCaseInformation 119 | 120 | for _, actorItem := range useCaseInformation { 121 | useCaseActor := actorItem.Actor 122 | useCaseSupport := actorItem.UseCaseSupport 123 | remoteDevice.SetUseCaseActor(string(*useCaseActor), useCaseSupport) 124 | 125 | if *actorItem.Actor == model.UseCaseActorType(model.UseCaseActorEnumTypeEV) { 126 | for _, item := range actorItem.UseCaseSupport { 127 | if f.Delegate != nil { 128 | useCaseAvailable := true 129 | if item.UseCaseAvailable != nil { 130 | useCaseAvailable = *item.UseCaseAvailable 131 | } 132 | f.Delegate.UpdateUseCaseSupportData(f, *item.UseCaseName, useCaseAvailable) 133 | } 134 | } 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (f *NodeManagement) replyUseCaseData(ctrl spine.Context, data model.NodeManagementUseCaseDataType) error { 142 | remoteDevice := ctrl.GetDevice() 143 | 144 | // Exmaple EV: {"data":[{"header":[{"protocolId":"ee1.0"}]},{"payload":{"datagram":[{"header":[{"specificationVersion":"1.1.1"},{"addressSource":[{"device":"d:_i:EVSE"},{"entity":[0]},{"feature":0}]},{"addressDestination":[{"device":"HEMS"},{"entity":[0]},{"feature":0}]},{"msgCounter":13484},{"cmdClassifier":"notify"}]},{"payload":[{"cmd":[[{"nodeManagementUseCaseData":[{"useCaseInformation":[[{"actor":"EV"},{"useCaseSupport":[[{"useCaseName":"measurementOfElectricityDuringEvCharging"},{"useCaseAvailable":true},{"scenarioSupport":[1,2,3]}],[{"useCaseName":"optimizationOfSelfConsumptionDuringEvCharging"},{"useCaseAvailable":true},{"scenarioSupport":[1,2,3]}],[{"useCaseName":"overloadProtectionByEvChargingCurrentCurtailment"},{"useCaseAvailable":true},{"scenarioSupport":[1,2,3]}],[{"useCaseName":"coordinatedEvCharging"},{"useCaseAvailable":true},{"scenarioSupport":[1,2,3,4,5,6,7,8]}],[{"useCaseName":"evCommissioningAndConfiguration"},{"useCaseAvailable":true},{"scenarioSupport":[1,2,3,4,5,6,7,8]}],[{"useCaseName":"evseCommissioningAndConfiguration"},{"useCaseAvailable":true},{"scenarioSupport":[1,2]}],[{"useCaseName":"evChargingSummary"},{"useCaseAvailable":true},{"scenarioSupport":[1]}],[{"useCaseName":"evStateOfCharge"},{"useCaseAvailable":false},{"scenarioSupport":[1]}]]}]]}]}]]}]}]}}]} 145 | 146 | useCaseInformation := data.UseCaseInformation 147 | if useCaseInformation == nil { 148 | return errors.New("nodemanagement.replyUseCaseData: invalid UseCaseInformation") 149 | } 150 | 151 | return f.updateSupportedUseCases(ctrl, remoteDevice, data) 152 | } 153 | 154 | func (f *NodeManagement) handleUseCaseData(ctrl spine.Context, op model.CmdClassifierType, data *model.NodeManagementUseCaseDataType, isPartialForCmd bool) error { 155 | switch op { 156 | case model.CmdClassifierTypeRead: 157 | return f.readUseCaseData(ctrl, *data) 158 | 159 | case model.CmdClassifierTypeReply: 160 | return f.replyUseCaseData(ctrl, *data) 161 | 162 | case model.CmdClassifierTypeNotify: 163 | return f.replyUseCaseData(ctrl, *data) 164 | 165 | default: 166 | return fmt.Errorf("nodemanagement.handleUseCaseData: NodeManagementUseCaseData CmdClassifierType not implemented: %s", op) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/evcc-io/eebus 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/fatih/structs v1.1.0 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/libp2p/zeroconf/v2 v2.2.0 9 | github.com/mitchellh/mapstructure v1.4.1 10 | github.com/rickb777/date v1.17.0 11 | github.com/samber/lo v1.21.0 12 | ) 13 | 14 | require ( 15 | github.com/miekg/dns v1.1.43 // indirect 16 | github.com/rickb777/plural v1.4.1 // indirect 17 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 18 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect 19 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 3 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 4 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 5 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 6 | github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv53Q= 7 | github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= 8 | github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= 9 | github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= 10 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 11 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 12 | github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/rickb777/date v1.17.0 h1:Qk1MUtTLFfIWYhRaNRyk1t7LmjfkjOEELacQPsoh7Nw= 15 | github.com/rickb777/date v1.17.0/go.mod h1:b3AnLwjEdg1YWLUFnAd/lUq3JDJmMRXi/Onm8q0zlQg= 16 | github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= 17 | github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= 18 | github.com/samber/lo v1.21.0 h1:FSby8pJQtX4KmyddTCCGhc3JvnnIVrDA+NW37rG+7G8= 19 | github.com/samber/lo v1.21.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= 20 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 21 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 22 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 23 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 24 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 25 | golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 26 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= 27 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 28 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 29 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 35 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 37 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 38 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 39 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 40 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 41 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 42 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 43 | -------------------------------------------------------------------------------- /mdns/service.go: -------------------------------------------------------------------------------- 1 | package mdns 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strings" 9 | 10 | "github.com/evcc-io/eebus/ship" 11 | "github.com/evcc-io/eebus/util" 12 | "github.com/gorilla/websocket" 13 | "github.com/libp2p/zeroconf/v2" 14 | "github.com/mitchellh/mapstructure" 15 | ) 16 | 17 | // ServiceDescription contains the ship service parameters 18 | type ServiceDescription struct { 19 | Model, Brand string 20 | SKI string 21 | Register bool 22 | Path string 23 | ID string 24 | } 25 | 26 | // Service is the ship service 27 | type Service struct { 28 | ServiceDescription 29 | URIs []string 30 | Conn *ship.Connector 31 | } 32 | 33 | // NewFromDNSEntry creates ship service from its DNS definition 34 | func NewFromDNSEntry(zc *zeroconf.ServiceEntry) (*Service, error) { 35 | ss := Service{} 36 | 37 | txtM := make(map[string]interface{}) 38 | for _, txtE := range zc.Text { 39 | split := strings.SplitN(txtE, "=", 2) 40 | if len(split) == 2 { 41 | txtM[split[0]] = split[1] 42 | } 43 | } 44 | 45 | decoderConfig := &mapstructure.DecoderConfig{ 46 | Result: &ss.ServiceDescription, 47 | WeaklyTypedInput: true, 48 | } 49 | 50 | decoder, err := mapstructure.NewDecoder(decoderConfig) 51 | if err == nil { 52 | err = decoder.Decode(txtM) 53 | if err != nil { 54 | return &ss, err 55 | } 56 | } 57 | 58 | ss.URIs, err = URIsFromDNS(zc, ss.ServiceDescription.Path) 59 | 60 | return &ss, err 61 | } 62 | 63 | // URIsFromDNS returns the service URI and appends the path 64 | func URIsFromDNS(zc *zeroconf.ServiceEntry, path string) ([]string, error) { 65 | var uris []string 66 | 67 | if len(zc.HostName) > 0 { 68 | uris = append(uris, createURI(zc.HostName, zc.Port)+path) 69 | } 70 | 71 | for _, address := range zc.AddrIPv4 { 72 | uris = append(uris, createURI(address.String(), zc.Port)+path) 73 | } 74 | 75 | for _, address := range zc.AddrIPv6 { 76 | uris = append(uris, createURI(address.String(), zc.Port)+path) 77 | } 78 | 79 | if len(uris) == 0 { 80 | return uris, errors.New("mDNS record doesn't contain an IP address") 81 | } 82 | 83 | return uris, nil 84 | } 85 | 86 | func createURI(host string, port int) string { 87 | return ship.Scheme + net.JoinHostPort(host, fmt.Sprintf("%d", port)) 88 | } 89 | 90 | // WebsocketConnector is the connector used for establishing new websocket connections 91 | var WebsocketConnector func(uri string) (*websocket.Conn, error) 92 | 93 | // Connect connects to the service endpoint and performs handshake 94 | func (ss *Service) Connect(log util.Logger, accessMethod string, cert tls.Certificate, closeHandler func(string)) (ship.Conn, error) { 95 | WebsocketConnector = ship.TLSConnection(cert) 96 | 97 | for _, uri := range ss.URIs { 98 | ws, err := WebsocketConnector(uri) 99 | if err != nil { 100 | log.Printf("Failed to connect to %s: %s\n", uri, err) 101 | continue 102 | } 103 | 104 | sc := &ship.Connector{ 105 | Log: log, 106 | Local: ship.Service{Pin: "", Methods: accessMethod}, 107 | Remote: ship.Service{Pin: ""}, 108 | CloseHandler: closeHandler, 109 | SKI: ss.ServiceDescription.SKI, 110 | } 111 | 112 | conn, err := sc.Connect(ws) 113 | if err == nil { 114 | return conn, nil 115 | } 116 | } 117 | 118 | return nil, errors.New("cannot connect to any service endpoint") 119 | } 120 | -------------------------------------------------------------------------------- /server/listener.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/evcc-io/eebus/cert" 8 | "github.com/evcc-io/eebus/ship" 9 | "github.com/evcc-io/eebus/util" 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | type Listener struct { 14 | Log util.Logger 15 | Handler func(ski string, conn ship.Conn) error 16 | AccessMethod string 17 | } 18 | 19 | func (s *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | if s.Log == nil { 21 | s.Log = &util.NopLogger{} 22 | } 23 | 24 | upgrader := websocket.Upgrader{ 25 | ReadBufferSize: 1024, 26 | WriteBufferSize: 1024, 27 | CheckOrigin: func(r *http.Request) bool { return true }, 28 | Subprotocols: []string{ship.SubProtocol}, 29 | } 30 | 31 | // upgrade 32 | ws, err := upgrader.Upgrade(w, r, nil) 33 | if err != nil { 34 | s.Log.Println(err) 35 | return 36 | } 37 | 38 | // return and close connection 39 | if ws.Subprotocol() != ship.SubProtocol { 40 | s.Log.Println("protocol mismatch:", ws.Subprotocol()) 41 | return 42 | } 43 | 44 | // ship 45 | shipSrv := &ship.Server{ 46 | Log: s.Log, 47 | Local: ship.Service{Pin: "", Methods: s.AccessMethod}, 48 | Remote: ship.Service{Pin: ""}, 49 | } 50 | 51 | conn, err := shipSrv.Serve(ws) 52 | if err != nil { 53 | s.Log.Println(err) 54 | return 55 | } 56 | 57 | if s.Handler == nil { 58 | _ = conn.Close() 59 | err = errors.New("no handler") 60 | } 61 | 62 | if err == nil { 63 | var ski string 64 | if len(r.TLS.PeerCertificates) > 0 { 65 | ski, err = cert.SkiFromX509(r.TLS.PeerCertificates[0]) 66 | } 67 | 68 | if err == nil { 69 | err = s.Handler(ski, conn) 70 | } 71 | } 72 | 73 | s.Log.Println("done:", err) 74 | } 75 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "strconv" 11 | 12 | "github.com/evcc-io/eebus/cert" 13 | "github.com/evcc-io/eebus/ship" 14 | "github.com/evcc-io/eebus/util" 15 | "github.com/libp2p/zeroconf/v2" 16 | ) 17 | 18 | type Server struct { 19 | Log util.Logger 20 | Addr, Path string 21 | ID, Brand, Model, Type string 22 | Interfaces []string 23 | Register bool 24 | Certificate tls.Certificate 25 | } 26 | 27 | func (c *Server) Announce() (*zeroconf.Server, error) { 28 | ski, err := cert.SkiFromCert(c.Certificate) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | path := c.Path 34 | if path == "" { 35 | path = "/" 36 | } 37 | 38 | _, port, err := net.SplitHostPort(c.Addr) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | portInt, err := strconv.Atoi(port) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | if c.Log != nil { 49 | c.Log.Printf("mDNS: announcing id: %s ski: %s", c.ID, ski) 50 | } 51 | 52 | var ifaces []net.Interface = nil 53 | if len(c.Interfaces) > 0 { 54 | ifaces = make([]net.Interface, len(c.Interfaces)) 55 | for i, iface := range c.Interfaces { 56 | ifaceInt, err := net.InterfaceByName(iface) 57 | if err != nil { 58 | return nil, err 59 | } 60 | ifaces[i] = *ifaceInt 61 | } 62 | } 63 | server, err := zeroconf.Register(c.Model, ship.ZeroconfType, ship.ZeroconfDomain, portInt, []string{ 64 | "txtvers=1", 65 | "path=" + path, 66 | "id=" + c.ID, 67 | "ski=" + ski, 68 | "brand=" + c.Brand, 69 | "model=" + c.Model, 70 | "type=" + c.Type, 71 | "register=" + fmt.Sprintf("%v", c.Register), 72 | }, ifaces) 73 | 74 | if err != nil { 75 | err = fmt.Errorf("mDNS: failed registering service: %w", err) 76 | } 77 | 78 | return server, err 79 | } 80 | 81 | func (c *Server) createVerifier(verifier func(*x509.Certificate) error) func(state tls.ConnectionState) error { 82 | return func(state tls.ConnectionState) error { 83 | if len(state.PeerCertificates) == 0 { 84 | return errors.New("missing client certificate") 85 | } 86 | 87 | cert := state.PeerCertificates[0] 88 | 89 | if len(cert.SubjectKeyId) == 0 { 90 | return errors.New("missing client ski") 91 | } 92 | 93 | return verifier(state.PeerCertificates[0]) 94 | } 95 | } 96 | 97 | func (c *Server) Listen(handler http.Handler, verifier func(*x509.Certificate) error) error { 98 | s := &http.Server{ 99 | Addr: c.Addr, 100 | Handler: handler, 101 | TLSConfig: &tls.Config{ 102 | Certificates: []tls.Certificate{c.Certificate}, 103 | ClientAuth: tls.RequireAnyClientCert, 104 | CipherSuites: ship.CipherSuites, 105 | }, 106 | } 107 | 108 | if verifier != nil { 109 | s.TLSConfig.VerifyConnection = c.createVerifier(verifier) 110 | } 111 | 112 | return s.ListenAndServeTLS("", "") 113 | } 114 | -------------------------------------------------------------------------------- /ship/client.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/evcc-io/eebus/ship/message" 9 | "github.com/evcc-io/eebus/ship/ship" 10 | "github.com/evcc-io/eebus/ship/transport" 11 | "github.com/evcc-io/eebus/util" 12 | "github.com/gorilla/websocket" 13 | ) 14 | 15 | // Connector is the ship client connector 16 | type Connector struct { 17 | Log util.Logger 18 | Local Service 19 | Remote Service 20 | CloseHandler func(string) 21 | SKI string 22 | 23 | // mux sync.Mutex 24 | closedHandlerInvoked bool 25 | } 26 | 27 | // init creates the connection 28 | func (c *Connector) init(t *transport.Transport) error { 29 | init := []byte{message.CmiTypeInit, 0x00} 30 | 31 | // CMI_STATE_CLIENT_SEND 32 | if err := t.WriteBinary(init); err != nil { 33 | return err 34 | } 35 | 36 | timer := time.NewTimer(message.CmiTimeout) 37 | 38 | // CMI_STATE_CLIENT_WAIT 39 | msg, err := t.ReadBinary(timer.C) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // CMI_STATE_CLIENT_EVALUATE 45 | if !bytes.Equal(init, msg) { 46 | return fmt.Errorf("init: invalid response") 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (c *Connector) protocolHandshake(t *transport.Transport) error { 53 | hs := ship.CmiMessageProtocolHandshake{ 54 | MessageProtocolHandshake: ship.MessageProtocolHandshake{ 55 | HandshakeType: ship.ProtocolHandshakeTypeTypeAnnouncemax, 56 | Version: ship.Version{Major: 1, Minor: 0}, 57 | Formats: ship.MessageProtocolFormatsType{ 58 | Format: []ship.MessageProtocolFormatType{ship.ProtocolHandshakeFormatJSON}, 59 | }, 60 | }, 61 | } 62 | if err := t.WriteJSON(message.CmiTypeControl, hs); err != nil { 63 | return fmt.Errorf("handshake: %w", err) 64 | } 65 | 66 | // receive server selection and send selection back to server 67 | err := t.HandshakeReceiveSelect() 68 | if err == nil { 69 | hs.MessageProtocolHandshake.HandshakeType = ship.ProtocolHandshakeTypeTypeSelect 70 | err = t.WriteJSON(message.CmiTypeControl, hs) 71 | } 72 | 73 | return err 74 | } 75 | 76 | // // Close performs ordered close of client connection 77 | // func (c *Connector) Close(t *transport.Transport) error { 78 | // c.mux.Lock() 79 | // defer c.mux.Unlock() 80 | 81 | // if c.closed { 82 | // return os.ErrClosed 83 | // } 84 | 85 | // c.closed = true 86 | 87 | // // stop readPump 88 | // // defer close(c.closeC) 89 | 90 | // return t.Close() 91 | // } 92 | 93 | // Connect performs the client connection handshake 94 | func (c *Connector) Connect(conn *websocket.Conn) (Conn, error) { 95 | t := transport.New(c.Log, conn) 96 | t.CloseHandler = c.TransportClosed 97 | 98 | if err := c.init(t); err != nil { 99 | return nil, err 100 | } 101 | 102 | err := t.Hello() 103 | if err == nil { 104 | err = c.protocolHandshake(t) 105 | } 106 | 107 | if err == nil { 108 | err = t.PinState( 109 | ship.PinValueType(c.Local.Pin), 110 | ship.PinValueType(c.Remote.Pin), 111 | ) 112 | } 113 | 114 | if err == nil { 115 | c.Remote.Methods, err = t.AccessMethodsRequest(c.Local.Methods) 116 | } 117 | 118 | // close connection if handshake or hello fails 119 | if err != nil { 120 | _ = t.Close() 121 | c.TransportClosed() 122 | } 123 | 124 | shipConn := &connection{t: t} 125 | 126 | return shipConn, err 127 | } 128 | 129 | // TransportClosed handles a closed transport conncection 130 | func (c *Connector) TransportClosed() { 131 | if c.CloseHandler != nil && !c.closedHandlerInvoked { 132 | // make sure the close handler is only invoked once for this connection 133 | c.closedHandlerInvoked = true 134 | c.CloseHandler(c.SKI) 135 | } 136 | } 137 | 138 | // Connect performs the client connection handshake 139 | func Connect(conn *websocket.Conn) (Conn, error) { 140 | c := &Connector{Log: &util.NopLogger{}} 141 | return c.Connect(conn) 142 | } 143 | -------------------------------------------------------------------------------- /ship/connection.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/evcc-io/eebus/ship/message" 11 | "github.com/evcc-io/eebus/ship/ship" 12 | "github.com/evcc-io/eebus/ship/transport" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | // TLSConnection creates an encrypted websocket connection 17 | func TLSConnection(cert tls.Certificate) func(uri string) (*websocket.Conn, error) { 18 | return func(uri string) (*websocket.Conn, error) { 19 | dialer := &websocket.Dialer{ 20 | Proxy: http.ProxyFromEnvironment, 21 | HandshakeTimeout: 5 * time.Second, 22 | TLSClientConfig: &tls.Config{ 23 | Certificates: []tls.Certificate{cert}, 24 | InsecureSkipVerify: true, 25 | CipherSuites: CipherSuites, 26 | }, 27 | Subprotocols: []string{SubProtocol}, 28 | } 29 | 30 | conn, _, err := dialer.Dial(uri, nil) 31 | 32 | return conn, err 33 | } 34 | } 35 | 36 | var ErrInvalidMessageType = errors.New("invalid message type") 37 | 38 | type Conn interface { 39 | Read() (json.RawMessage, error) 40 | Write(json.RawMessage) error 41 | Close() error 42 | IsConnectionClosed() bool 43 | } 44 | 45 | var _ Conn = (*connection)(nil) 46 | 47 | type connection struct { 48 | t *transport.Transport 49 | } 50 | 51 | func (c *connection) IsConnectionClosed() bool { 52 | return c.t.IsConnectionClosed() 53 | } 54 | 55 | func (c *connection) Read() (json.RawMessage, error) { 56 | msg, err := c.t.ReadMessage(nil) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | switch typed := msg.(type) { 62 | case ship.Data: 63 | return typed.Payload, nil 64 | 65 | case ship.ConnectionClose: 66 | return nil, c.t.AcceptClose() 67 | 68 | default: 69 | err = ErrInvalidMessageType 70 | } 71 | 72 | return nil, err 73 | } 74 | 75 | func (c *connection) Write(payload json.RawMessage) error { 76 | hs := ship.CmiData{ 77 | Data: ship.Data{ 78 | Header: ship.HeaderType{ 79 | ProtocolId: ship.ProtocolIdType(message.ProtocolID), 80 | }, 81 | Payload: payload, 82 | }, 83 | } 84 | 85 | return c.t.WriteJSON(message.CmiTypeData, &hs) 86 | } 87 | 88 | func (c *connection) Close() error { 89 | return c.t.Close() 90 | } 91 | -------------------------------------------------------------------------------- /ship/const.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | import "crypto/tls" 4 | 5 | // protocol constants 6 | const ( 7 | Scheme = "wss://" 8 | SubProtocol = "ship" 9 | ZeroconfType = "_ship._tcp" 10 | ZeroconfDomain = "local." 11 | ) 12 | 13 | // CipherSuites are the SHIP cipher suites 14 | var CipherSuites = []uint16{ 15 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 16 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, 17 | } 18 | -------------------------------------------------------------------------------- /ship/message/decode.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/evcc-io/eebus/ship/ship" 8 | ) 9 | 10 | func Decode(b []byte) (interface{}, error) { 11 | var sum map[string]json.RawMessage 12 | 13 | if err := json.Unmarshal(b, &sum); err != nil { 14 | return nil, err 15 | } 16 | 17 | var typ string 18 | var raw json.RawMessage 19 | for k, v := range sum { 20 | typ = k 21 | raw = v 22 | } 23 | 24 | switch typ { 25 | case "accessMethods": 26 | res := []ship.AccessMethods{} 27 | err := json.Unmarshal(raw, &res) 28 | if len(res) > 0 { 29 | return res[0], err 30 | } 31 | return ship.AccessMethods{}, nil 32 | 33 | case "accessMethodsRequest": 34 | res := []ship.AccessMethodsRequest{} 35 | err := json.Unmarshal(raw, &res) 36 | if len(res) > 0 { 37 | return res[0], err 38 | } 39 | return ship.AccessMethodsRequest{}, nil 40 | 41 | case "connectionPinState": 42 | res := []ship.ConnectionPinState{} 43 | err := json.Unmarshal(raw, &res) 44 | if len(res) > 0 { 45 | return res[0], err 46 | } 47 | return ship.ConnectionPinState{}, nil 48 | 49 | case "connectionPinInput": 50 | res := []ship.ConnectionPinInput{} 51 | err := json.Unmarshal(raw, &res) 52 | if len(res) > 0 { 53 | return res[0], err 54 | } 55 | return ship.ConnectionPinInput{}, nil 56 | 57 | case "connectionPinError": 58 | res := []ship.ConnectionPinError{} 59 | err := json.Unmarshal(raw, &res) 60 | if len(res) > 0 { 61 | return res[0], err 62 | } 63 | return ship.ConnectionPinError{}, nil 64 | 65 | case "connectionHello": 66 | res := []ship.ConnectionHello{} 67 | err := json.Unmarshal(raw, &res) 68 | if len(res) > 0 { 69 | return res[0], err 70 | } 71 | return ship.ConnectionHello{}, nil 72 | 73 | case "connectionClose": 74 | res := []ship.ConnectionClose{} 75 | err := json.Unmarshal(raw, &res) 76 | if len(res) > 0 { 77 | return res[0], err 78 | } 79 | return ship.ConnectionClose{}, nil 80 | 81 | case "messageProtocolHandshake": 82 | res := ship.MessageProtocolHandshake{} 83 | err := json.Unmarshal(raw, &res) 84 | return res, err 85 | 86 | case "data": 87 | res := ship.Data{} 88 | err := json.Unmarshal(raw, &res) 89 | return res, err 90 | 91 | default: 92 | return nil, errors.New("invalid type") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ship/message/types.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "time" 4 | 5 | const ( 6 | CmiTypeInit byte = 0 7 | CmiTypeControl byte = 1 8 | CmiTypeData byte = 2 9 | CmiTypeEnd byte = 3 10 | 11 | CmiTimeout = 60 * time.Second 12 | CmiHelloProlongationTimeout = 30 * time.Second 13 | CmiCloseTimeout = 100 * time.Millisecond 14 | 15 | ProtocolID = "ee1.0" 16 | ) 17 | -------------------------------------------------------------------------------- /ship/server.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/evcc-io/eebus/ship/message" 10 | "github.com/evcc-io/eebus/ship/ship" 11 | "github.com/evcc-io/eebus/ship/transport" 12 | "github.com/evcc-io/eebus/util" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | // Server is the SHIP server 17 | type Server struct { 18 | Log util.Logger 19 | Local Service 20 | Remote Service 21 | } 22 | 23 | // Init creates the connection 24 | func (c *Server) init(t *transport.Transport) error { 25 | timer := time.NewTimer(message.CmiTimeout) 26 | 27 | // CMI_STATE_SERVER_WAIT 28 | msg, err := t.ReadBinary(timer.C) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // CMI_STATE_SERVER_EVALUATE 34 | init := []byte{message.CmiTypeInit, 0x00} 35 | if !bytes.Equal(init, msg) { 36 | return fmt.Errorf("init: invalid response") 37 | } 38 | 39 | return t.WriteBinary(init) 40 | } 41 | 42 | func (c *Server) protocolHandshake(t *transport.Transport) error { 43 | timer := time.NewTimer(transport.CmiReadWriteTimeout) 44 | msg, err := t.ReadMessage(timer.C) 45 | if err != nil { 46 | if errors.Is(err, transport.ErrTimeout) { 47 | _ = t.WriteJSON(message.CmiTypeControl, ship.CmiMessageProtocolHandshakeError{ 48 | MessageProtocolHandshakeError: ship.MessageProtocolHandshakeError{ 49 | Error: "2", // TODO 50 | }}) 51 | } 52 | 53 | return err 54 | } 55 | 56 | switch typed := msg.(type) { 57 | case ship.MessageProtocolHandshake: 58 | if typed.HandshakeType != ship.ProtocolHandshakeTypeTypeAnnouncemax || !typed.Formats.IsSupported(ship.ProtocolHandshakeFormatJSON) { 59 | msg := ship.CmiMessageProtocolHandshakeError{ 60 | MessageProtocolHandshakeError: ship.MessageProtocolHandshakeError{ 61 | Error: "2", // TODO 62 | }, 63 | } 64 | 65 | _ = t.WriteJSON(message.CmiTypeControl, msg) 66 | err = errors.New("handshake: invalid response") 67 | break 68 | } 69 | 70 | // send selection to client 71 | typed.HandshakeType = ship.ProtocolHandshakeTypeTypeSelect 72 | err = t.WriteJSON(message.CmiTypeControl, ship.CmiMessageProtocolHandshake{ 73 | MessageProtocolHandshake: typed, 74 | }) 75 | 76 | default: 77 | return fmt.Errorf("handshake: invalid type") 78 | } 79 | 80 | // receive selection back from client 81 | if err == nil { 82 | err = t.HandshakeReceiveSelect() 83 | } 84 | 85 | return err 86 | } 87 | 88 | // Close performs ordered close of server connection 89 | // func (c *Server) Close() error { 90 | // return t.Close() 91 | // } 92 | 93 | // Serve performs the server connection handshake 94 | func (c *Server) Serve(conn *websocket.Conn) (Conn, error) { 95 | t := transport.New(c.Log, conn) 96 | 97 | if err := c.init(t); err != nil { 98 | return nil, err 99 | } 100 | 101 | // CMI_STATE_DATA_PREPARATION 102 | err := t.Hello() 103 | 104 | if err == nil { 105 | err = c.protocolHandshake(t) 106 | } 107 | 108 | if err == nil { 109 | err = t.PinState( 110 | ship.PinValueType(c.Local.Pin), 111 | ship.PinValueType(c.Remote.Pin), 112 | ) 113 | } 114 | 115 | if err == nil { 116 | c.Remote.Methods, err = t.AccessMethodsRequest(c.Local.Methods) 117 | } 118 | 119 | // close connection if handshake or hello fails 120 | if err != nil { 121 | _ = t.Close() 122 | } 123 | 124 | shipConn := &connection{t: t} 125 | 126 | return shipConn, err 127 | } 128 | -------------------------------------------------------------------------------- /ship/service.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | // Service is the service description 4 | type Service struct { 5 | Pin string 6 | Methods string 7 | } 8 | -------------------------------------------------------------------------------- /ship/ship/format.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | import ( 4 | "github.com/samber/lo" 5 | ) 6 | 7 | const ProtocolHandshakeFormatJSON MessageProtocolFormatType = "JSON-UTF8" 8 | 9 | // IsSupported validates if format is supported 10 | func (m MessageProtocolFormatsType) IsSupported(format MessageProtocolFormatType) bool { 11 | return lo.Contains(m.Format, format) 12 | } 13 | -------------------------------------------------------------------------------- /ship/transport/accessmethods.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/evcc-io/eebus/ship/message" 8 | "github.com/evcc-io/eebus/ship/ship" 9 | ) 10 | 11 | // AccessMethodsRequest sends access methods request and processes answer 12 | func (c *Transport) AccessMethodsRequest(methods string) (string, error) { 13 | err := c.WriteJSON(message.CmiTypeControl, ship.CmiAccessMethodsRequest{ 14 | AccessMethodsRequest: ship.AccessMethodsRequest{}, 15 | }) 16 | 17 | for err == nil { 18 | timer := time.NewTimer(CmiReadWriteTimeout) 19 | 20 | var msg interface{} 21 | msg, err = c.ReadMessage(timer.C) 22 | if err != nil { 23 | break 24 | } 25 | 26 | switch typed := msg.(type) { 27 | case ship.AccessMethods: 28 | // access methods received 29 | return typed.Id, nil 30 | 31 | case ship.AccessMethodsRequest: 32 | err = c.WriteJSON(message.CmiTypeControl, ship.CmiAccessMethods{ 33 | AccessMethods: ship.AccessMethods{Id: methods}, 34 | }) 35 | 36 | default: 37 | err = errors.New("access methods: invalid type") 38 | } 39 | } 40 | 41 | return "", err 42 | } 43 | -------------------------------------------------------------------------------- /ship/transport/close.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/evcc-io/eebus/ship/message" 8 | "github.com/evcc-io/eebus/ship/ship" 9 | ) 10 | 11 | // AcceptClose accepts connection close 12 | func (c *Transport) AcceptClose() error { 13 | err := c.WriteJSON(message.CmiTypeEnd, ship.CmiConnectionClose{ 14 | ConnectionClose: ship.ConnectionClose{ 15 | Phase: ship.ConnectionClosePhaseTypeConfirm, 16 | }, 17 | }) 18 | 19 | // stop read/write pump 20 | if !c.isChannelClosed() { 21 | close(c.closeC) 22 | } 23 | c.conn.Close() 24 | c.handleConnectionClose() 25 | 26 | return err 27 | } 28 | 29 | // Close closes the connection 30 | func (c *Transport) Close() error { 31 | err := c.WriteJSON(message.CmiTypeEnd, ship.CmiConnectionClose{ 32 | ConnectionClose: ship.ConnectionClose{ 33 | Phase: ship.ConnectionClosePhaseTypeAnnounce, 34 | // MaxTime: int(ship.CmiCloseTimeout / time.Millisecond), 35 | }, 36 | }) 37 | 38 | timer := time.NewTimer(message.CmiCloseTimeout) 39 | for err == nil { 40 | var msg interface{} 41 | msg, err = c.ReadMessage(timer.C) 42 | if err != nil { 43 | break 44 | } 45 | 46 | if typed, ok := msg.(ship.ConnectionClose); ok && typed.Phase == ship.ConnectionClosePhaseTypeConfirm { 47 | break 48 | } 49 | 50 | err = errors.New("close: invalid response") 51 | } 52 | 53 | // stop read/write pump 54 | if !c.isChannelClosed() { 55 | close(c.closeC) 56 | } 57 | c.conn.Close() 58 | c.handleConnectionClose() 59 | 60 | return err 61 | } 62 | 63 | func (c *Transport) isChannelClosed() bool { 64 | select { 65 | case <-c.closeC: 66 | return false 67 | default: 68 | return true 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ship/transport/data.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/evcc-io/eebus/ship/ship" 8 | ) 9 | 10 | // DataReceive receives handshake 11 | func (c *Transport) DataReceive() error { 12 | timer := time.NewTimer(CmiReadWriteTimeout) 13 | msg, err := c.ReadMessage(timer.C) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | switch typed := msg.(type) { 19 | case ship.Data: 20 | _ = typed 21 | return nil 22 | 23 | case ship.ConnectionClose: 24 | err = errors.New("data: remote closed") 25 | 26 | default: 27 | err = errors.New("data: invalid type") 28 | } 29 | 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /ship/transport/handshake.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/evcc-io/eebus/ship/message" 8 | "github.com/evcc-io/eebus/ship/ship" 9 | ) 10 | 11 | // HandshakeReceiveSelect receives handshake 12 | func (c *Transport) HandshakeReceiveSelect() error { 13 | timer := time.NewTimer(CmiReadWriteTimeout) 14 | msg, err := c.ReadMessage(timer.C) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | switch typed := msg.(type) { 20 | case ship.MessageProtocolHandshake: 21 | if typed.HandshakeType != ship.ProtocolHandshakeTypeTypeSelect || !typed.Formats.IsSupported(ship.ProtocolHandshakeFormatJSON) { 22 | _ = c.WriteJSON(message.CmiTypeControl, ship.CmiMessageProtocolHandshakeError{ 23 | MessageProtocolHandshakeError: ship.MessageProtocolHandshakeError{ 24 | Error: "2", // TODO 25 | }}) 26 | 27 | err = errors.New("handshake: invalid format") 28 | } 29 | 30 | return err 31 | 32 | case ship.ConnectionClose: 33 | err = errors.New("handshake: remote closed") 34 | 35 | default: 36 | err = errors.New("handshake: invalid type") 37 | } 38 | 39 | return err 40 | } 41 | -------------------------------------------------------------------------------- /ship/transport/hello.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/evcc-io/eebus/ship/message" 8 | "github.com/evcc-io/eebus/ship/ship" 9 | ) 10 | 11 | // Hello is the common hello exchange 12 | func (c *Transport) Hello() error { 13 | waitMs := uint(message.CmiTimeout / time.Millisecond) 14 | 15 | // SME_HELLO_STATE_READY_INIT 16 | err := c.WriteJSON(message.CmiTypeControl, ship.CmiConnectionHello{ 17 | ConnectionHello: ship.ConnectionHello{ 18 | Phase: ship.ConnectionHelloPhaseTypeReady, 19 | Waiting: &waitMs, 20 | }, 21 | }) 22 | 23 | timer := time.NewTimer(message.CmiTimeout) 24 | for err == nil { 25 | // SME_HELLO_STATE_READY_LISTEN 26 | var msg interface{} 27 | msg, err = c.ReadMessage(timer.C) 28 | if err != nil { 29 | if errors.Is(err, ErrTimeout) { 30 | // SME_HELLO_STATE_READY_TIMEOUT 31 | _ = c.WriteJSON(message.CmiTypeControl, ship.CmiConnectionHello{ 32 | ConnectionHello: ship.ConnectionHello{ 33 | Phase: ship.ConnectionHelloPhaseTypeAborted, 34 | }, 35 | }) 36 | } 37 | 38 | return err 39 | } 40 | 41 | switch hello := msg.(type) { 42 | case ship.ConnectionHello: 43 | switch hello.Phase { 44 | case ship.ConnectionHelloPhaseTypeReady: 45 | // HELLO_OK 46 | return nil 47 | 48 | case ship.ConnectionHelloPhaseTypeAborted: 49 | err = errors.New("hello: aborted") 50 | 51 | case ship.ConnectionHelloPhaseTypePending: 52 | if hello.ProlongationRequest != nil && *hello.ProlongationRequest { 53 | timer = time.NewTimer(message.CmiHelloProlongationTimeout) 54 | } 55 | } 56 | 57 | case ship.ConnectionClose: 58 | err = errors.New("hello: remote closed") 59 | 60 | default: 61 | err = errors.New("hello: invalid type") 62 | } 63 | } 64 | 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /ship/transport/pin.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/evcc-io/eebus/ship/message" 8 | "github.com/evcc-io/eebus/ship/ship" 9 | ) 10 | 11 | // read pin requirements 12 | func (c *Transport) readPinState() (ship.ConnectionPinState, error) { 13 | timer := time.NewTimer(CmiReadWriteTimeout) 14 | msg, err := c.ReadMessage(timer.C) 15 | 16 | switch typed := msg.(type) { 17 | case ship.ConnectionPinState: 18 | return typed, err 19 | 20 | default: 21 | if err == nil { 22 | err = errors.New("pin: invalid type") 23 | } 24 | 25 | return ship.ConnectionPinState{}, err 26 | } 27 | } 28 | 29 | const ( 30 | pinReceived = 1 << iota 31 | pinSent 32 | 33 | pinCompleted = pinReceived | pinSent 34 | ) 35 | 36 | // PinState handles pin exchange 37 | func (c *Transport) PinState(local, remote ship.PinValueType) error { 38 | pinState := ship.ConnectionPinState{ 39 | PinState: ship.PinStateTypeNone, 40 | } 41 | 42 | var status int 43 | if local != "" { 44 | ok := ship.PinInputPermissionTypeOk 45 | pinState.PinState = ship.PinStateTypeRequired 46 | pinState.InputPermission = &ok 47 | } else { 48 | // always received if not necessary 49 | status |= pinReceived 50 | } 51 | 52 | err := c.WriteJSON(message.CmiTypeControl, ship.CmiConnectionPinState{ 53 | ConnectionPinState: pinState, 54 | }) 55 | 56 | timer := time.NewTimer(10 * time.Second) 57 | for err == nil && status != pinCompleted { 58 | var msg interface{} 59 | msg, err = c.ReadMessage(timer.C) 60 | if err != nil { 61 | break 62 | } 63 | 64 | switch typed := msg.(type) { 65 | // local pin 66 | case ship.ConnectionPinInput: 67 | // signal error to client 68 | if typed.Pin != local { 69 | err = c.WriteJSON(message.CmiTypeControl, ship.CmiConnectionPinError{ 70 | ConnectionPinError: ship.ConnectionPinError{ 71 | Error: "1", // TODO 72 | }, 73 | }) 74 | } 75 | 76 | status |= pinReceived 77 | 78 | // remote pin 79 | case ship.ConnectionPinState: 80 | if typed.PinState == ship.PinStateTypeOptional || typed.PinState == ship.PinStateTypeRequired { 81 | if remote != "" { 82 | err = c.WriteJSON(message.CmiTypeControl, ship.CmiConnectionPinInput{ 83 | ConnectionPinInput: ship.ConnectionPinInput{ 84 | Pin: remote, 85 | }, 86 | }) 87 | } else { 88 | err = errors.New("pin: remote pin required") 89 | } 90 | } 91 | 92 | status |= pinSent 93 | 94 | case ship.ConnectionPinError: 95 | err = errors.New("pin: remote pin mismatched") 96 | 97 | case ship.ConnectionClose: 98 | err = errors.New("pin: remote closed") 99 | 100 | default: 101 | err = errors.New("pin: invalid type") 102 | } 103 | } 104 | 105 | return err 106 | } 107 | -------------------------------------------------------------------------------- /ship/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "time" 10 | 11 | "github.com/evcc-io/eebus/ship/message" 12 | "github.com/evcc-io/eebus/util" 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | // CmiReadWriteTimeout timeout 17 | const CmiReadWriteTimeout = 10 * time.Second 18 | 19 | // ErrTimeout is the timeout error 20 | var ErrTimeout = errors.New("timeout") 21 | 22 | // Transport is the physical transport layer 23 | type Transport struct { 24 | conn *websocket.Conn 25 | logger util.Logger 26 | 27 | recv chan []byte 28 | recvErr chan error 29 | send chan []byte 30 | sendErr chan error 31 | closeC chan struct{} 32 | 33 | CloseHandler func() 34 | isClosed bool 35 | } 36 | 37 | const ( 38 | // Time allowed to write a message to the peer. 39 | writeWait = 10 * time.Second 40 | 41 | // Time allowed to read the next pong message from the peer. 42 | pongWait = 60 * time.Second 43 | 44 | // Send pings to peer with this period. Must be less than pongWait. 45 | pingPeriod = (pongWait * 9) / 10 46 | ) 47 | 48 | // New creates SHIP transport on given websocket connection 49 | func New(log util.Logger, conn *websocket.Conn) *Transport { 50 | t := &Transport{ 51 | conn: conn, 52 | logger: log, 53 | send: make(chan []byte, 1), 54 | recv: make(chan []byte, 1), 55 | sendErr: make(chan error, 1), 56 | recvErr: make(chan error, 1), 57 | closeC: make(chan struct{}), 58 | } 59 | 60 | go t.readPump() 61 | go t.writePump() 62 | 63 | return t 64 | } 65 | 66 | func (c *Transport) IsConnectionClosed() bool { 67 | return c.isClosed 68 | } 69 | 70 | func (c *Transport) handleConnectionClose() { 71 | c.isClosed = true 72 | if c.CloseHandler != nil { 73 | c.CloseHandler() 74 | } 75 | } 76 | 77 | func (c *Transport) log() util.Logger { 78 | if c.logger == nil { 79 | return &util.NopLogger{} 80 | } 81 | return c.logger 82 | } 83 | 84 | func (c *Transport) readPump() { 85 | defer func() { 86 | c.conn.Close() 87 | c.handleConnectionClose() 88 | }() 89 | 90 | _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)) 91 | c.conn.SetPongHandler(func(string) error { 92 | _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)) 93 | return nil 94 | }) 95 | 96 | for { 97 | select { 98 | case <-c.closeC: 99 | return 100 | 101 | default: 102 | typ, b, err := c.conn.ReadMessage() 103 | if err == nil { 104 | if len(b) > 2 { 105 | b = bytes.TrimSuffix(b, []byte{0x00}) // workaround 106 | c.log().Println("recv:", string(b[1:])) 107 | } 108 | 109 | if typ != websocket.BinaryMessage { 110 | err = fmt.Errorf("invalid message type: %d", typ) 111 | } 112 | } 113 | 114 | if err == nil { 115 | c.recv <- b 116 | } else { 117 | c.recvErr <- err 118 | } 119 | } 120 | } 121 | } 122 | 123 | // ReadBinary reads binary message 124 | func (c *Transport) ReadBinary(timerC <-chan time.Time) ([]byte, error) { 125 | select { 126 | case <-timerC: 127 | c.handleConnectionClose() 128 | return nil, ErrTimeout 129 | 130 | case <-c.closeC: 131 | c.handleConnectionClose() 132 | return nil, net.ErrClosed 133 | 134 | case b := <-c.recv: 135 | return b, nil 136 | 137 | case err := <-c.recvErr: 138 | return nil, err 139 | } 140 | } 141 | 142 | // ReadMessage reads JSON message 143 | func (c *Transport) ReadMessage(timerC <-chan time.Time) (interface{}, error) { 144 | select { 145 | case <-timerC: 146 | c.handleConnectionClose() 147 | return nil, ErrTimeout 148 | 149 | case <-c.closeC: 150 | c.handleConnectionClose() 151 | return nil, net.ErrClosed 152 | 153 | case b := <-c.recv: 154 | if len(b) < 2 { 155 | return nil, errors.New("invalid length") 156 | } 157 | if b[0] < 1 { 158 | return nil, errors.New("invalid phase") 159 | } 160 | 161 | return message.Decode(b[1:]) 162 | 163 | case err := <-c.recvErr: 164 | return nil, err 165 | } 166 | } 167 | 168 | // writePump pumps messages from the hub to the websocket connection. 169 | // 170 | // A goroutine running writePump is started for each connection. The 171 | // application ensures that there is at most one writer to a connection by 172 | // executing all writes from this goroutine. 173 | func (c *Transport) writePump() { 174 | ticker := time.NewTicker(pingPeriod) 175 | 176 | defer func() { 177 | ticker.Stop() 178 | c.conn.Close() 179 | c.handleConnectionClose() 180 | }() 181 | 182 | for { 183 | select { 184 | case msg, ok := <-c.send: 185 | _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 186 | if !ok { 187 | // The hub closed the channel. 188 | _ = c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 189 | return 190 | } 191 | 192 | if len(msg) > 2 { 193 | c.log().Println("send:", string(msg[1:])) 194 | } 195 | 196 | err := c.conn.WriteMessage(websocket.BinaryMessage, msg) 197 | c.sendErr <- err 198 | 199 | case <-ticker.C: 200 | _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 201 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 202 | return 203 | } 204 | } 205 | } 206 | } 207 | 208 | // WriteBinary writes binary message 209 | func (c *Transport) WriteBinary(msg []byte) error { 210 | if c.isClosed { 211 | return errors.New("cannot write to closed connection") 212 | } 213 | 214 | c.send <- msg 215 | 216 | timer := time.NewTimer(10 * time.Second) 217 | select { 218 | case <-timer.C: 219 | c.handleConnectionClose() 220 | return ErrTimeout 221 | 222 | case <-c.closeC: 223 | c.handleConnectionClose() 224 | return net.ErrClosed 225 | 226 | case err := <-c.sendErr: 227 | return err 228 | } 229 | } 230 | 231 | // WriteJSON writes JSON message 232 | func (c *Transport) WriteJSON(typ byte, jsonMsg interface{}) error { 233 | msg, err := json.Marshal(jsonMsg) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | // add header 239 | b := bytes.NewBuffer([]byte{typ}) 240 | if _, err = b.Write(msg); err == nil { 241 | err = c.WriteBinary(b.Bytes()) 242 | } 243 | 244 | return err 245 | } 246 | -------------------------------------------------------------------------------- /ship/uniqueid.go: -------------------------------------------------------------------------------- 1 | package ship 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // UniqueID creates ship ID with given prefix and salted by provided protectedID 9 | // The protectedID is basically the same as `machineid.ProtectedID(appId)` above 10 | // but provided by the system using this library 11 | func UniqueIDWithProtectedID(prefix, protectedID string) (string, error) { 12 | if len(protectedID) == 0 { 13 | return "", errors.New("A generated machine specific protectedID needs to be provided") 14 | } 15 | 16 | return fmt.Sprintf("%s-%0x", prefix, protectedID[:8]), nil 17 | } 18 | -------------------------------------------------------------------------------- /spine/context.go: -------------------------------------------------------------------------------- 1 | package spine 2 | 3 | import "github.com/evcc-io/eebus/spine/model" 4 | 5 | type Context interface { 6 | CloseConnectionBecauseOfError(err error) 7 | SetDevice(Device) 8 | GetDevice() Device 9 | UpdateDevice(model.NetworkManagementStateChangeType) 10 | HeartbeatCounter() *uint64 11 | Subscribe(lf Feature, rf Feature, typ model.FeatureTypeType) error 12 | ProcessSequenceFlowRequest(featureType model.FeatureTypeEnumType, functionType model.FunctionEnumType, cmdClassifier model.CmdClassifierType) (*model.MsgCounterType, error) 13 | Request(model.CmdClassifierType, model.FeatureAddressType, model.FeatureAddressType, bool, []model.CmdType) (*model.MsgCounterType, error) 14 | Reply(model.CmdClassifierType, model.CmdType) error 15 | Notify(senderAddress, destinationAddress *model.FeatureAddressType, cmd []model.CmdType) error 16 | Write(senderAddress, destinationAddress *model.FeatureAddressType, cmd []model.CmdType) error 17 | AddressSource() *model.FeatureAddressType 18 | AddSubscription(data model.SubscriptionManagementRequestCallType) error 19 | RemoveSubscription(data model.SubscriptionManagementDeleteCallType) error 20 | Subscriptions() []model.SubscriptionManagementEntryDataType 21 | } 22 | -------------------------------------------------------------------------------- /spine/device.go: -------------------------------------------------------------------------------- 1 | package spine 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | 9 | "github.com/evcc-io/eebus/spine/model" 10 | ) 11 | 12 | type Device interface { 13 | GetAddress() model.AddressDeviceType 14 | GetEntities() []Entity 15 | GetType() model.DeviceTypeType 16 | 17 | Add(e Entity) 18 | RemoveByAddress(addr []model.AddressEntityType) 19 | Entity(addr []model.AddressEntityType) Entity 20 | EntityByType(typ model.EntityTypeType) Entity 21 | 22 | SetUseCaseActor(actorName string, useCases []model.UseCaseSupportType) 23 | ResetUseCaseActors() 24 | GetUseCaseActors() []string 25 | GetUseCaseForActor(actorName string) ([]model.UseCaseSupportType, error) 26 | 27 | Information() *model.NodeManagementDetailedDiscoveryDeviceInformationType 28 | Dump(w io.Writer) 29 | } 30 | 31 | var _ Device = (*DeviceImpl)(nil) 32 | 33 | type DeviceImpl struct { 34 | Address model.AddressDeviceType 35 | Type model.DeviceTypeType 36 | Entities []Entity 37 | ActorUseCases map[string][]model.UseCaseSupportType 38 | } 39 | 40 | func (d *DeviceImpl) SetUseCaseActor(actorName string, useCases []model.UseCaseSupportType) { 41 | if d.ActorUseCases == nil { 42 | d.ActorUseCases = make(map[string][]model.UseCaseSupportType) 43 | } 44 | d.ActorUseCases[actorName] = useCases 45 | } 46 | 47 | func (d *DeviceImpl) ResetUseCaseActors() { 48 | d.ActorUseCases = nil 49 | } 50 | 51 | func (d *DeviceImpl) GetUseCaseActors() []string { 52 | var actors []string 53 | for actorName := range d.ActorUseCases { 54 | actors = append(actors, actorName) 55 | } 56 | return actors 57 | } 58 | 59 | func (d *DeviceImpl) GetUseCaseForActor(actorName string) ([]model.UseCaseSupportType, error) { 60 | usecases, found := d.ActorUseCases[actorName] 61 | if found { 62 | return usecases, nil 63 | } 64 | return nil, errors.New("actor not found") 65 | } 66 | 67 | func (d *DeviceImpl) GetAddress() model.AddressDeviceType { 68 | return d.Address 69 | } 70 | 71 | func (d *DeviceImpl) GetEntities() []Entity { 72 | return d.Entities 73 | } 74 | 75 | func (d *DeviceImpl) GetType() model.DeviceTypeType { 76 | return d.Type 77 | } 78 | 79 | func (d *DeviceImpl) Add(e Entity) { 80 | e.SetDevice(d) 81 | d.Entities = append(d.Entities, e) 82 | } 83 | 84 | func (d *DeviceImpl) RemoveByAddress(addr []model.AddressEntityType) { 85 | entityForRemoval := d.Entity(addr) 86 | 87 | var newEntities []Entity 88 | for _, item := range d.Entities { 89 | if !reflect.DeepEqual(item, entityForRemoval) { 90 | newEntities = append(newEntities, item) 91 | } 92 | } 93 | 94 | d.Entities = newEntities 95 | } 96 | 97 | func (d *DeviceImpl) Entity(id []model.AddressEntityType) Entity { 98 | for _, e := range d.Entities { 99 | if reflect.DeepEqual(id, e.GetAddress()) { 100 | return e 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | func (d *DeviceImpl) EntityByType(typ model.EntityTypeType) Entity { 107 | if d != nil { 108 | for _, e := range d.Entities { 109 | if e.GetType() == typ { 110 | return e 111 | } 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func (d *DeviceImpl) Information() *model.NodeManagementDetailedDiscoveryDeviceInformationType { 118 | res := model.NodeManagementDetailedDiscoveryDeviceInformationType{ 119 | Description: &model.NetworkManagementDeviceDescriptionDataType{ 120 | DeviceAddress: &model.DeviceAddressType{ 121 | Device: &d.Address, 122 | }, 123 | DeviceType: &d.Type, 124 | // TODO NetworkFeatureSet 125 | // NetworkFeatureSet: &smart, 126 | }, 127 | } 128 | return &res 129 | } 130 | 131 | func (d *DeviceImpl) Dump(w io.Writer) { 132 | fmt.Fprintf(w, "Details: device=%s, type=%s\n", d.Address, d.Type) 133 | 134 | fmt.Fprintln(w, " Entities:") 135 | for _, e := range d.Entities { 136 | addr := EntityAddressString(e) 137 | fmt.Fprintf(w, " e[%s] type=%s\n", addr, e.GetType()) 138 | } 139 | 140 | fmt.Fprintln(w, " Features:") 141 | for _, e := range d.Entities { 142 | e.Dump(w) 143 | } 144 | } 145 | 146 | func UnmarshalDevice(data model.NodeManagementDetailedDiscoveryDataType) *DeviceImpl { 147 | var dev *DeviceImpl 148 | 149 | if di := data.DeviceInformation; di != nil && di.Description != nil { 150 | dev = new(DeviceImpl) 151 | did := di.Description 152 | 153 | if did.DeviceType != nil { 154 | dev.Type = *did.DeviceType 155 | } 156 | 157 | if did.DeviceAddress != nil && did.DeviceAddress.Device != nil { 158 | dev.Address = *did.DeviceAddress.Device 159 | } 160 | } 161 | 162 | return dev 163 | } 164 | -------------------------------------------------------------------------------- /spine/entity.go: -------------------------------------------------------------------------------- 1 | package spine 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/evcc-io/eebus/spine/model" 10 | "github.com/samber/lo" 11 | ) 12 | 13 | type Entity interface { 14 | GetAddress() []model.AddressEntityType 15 | SetAddress([]model.AddressEntityType) 16 | 17 | GetDevice() Device 18 | SetDevice(d Device) 19 | 20 | GetFeatures() []Feature 21 | GetType() model.EntityTypeType 22 | 23 | GetManufacturerData() model.DeviceClassificationManufacturerDataType 24 | SetManufacturerData(model.DeviceClassificationManufacturerDataType) 25 | 26 | GetOperationState() model.DeviceDiagnosisOperatingStateType 27 | SetOperationState(model.DeviceDiagnosisOperatingStateType) 28 | 29 | Add(f Feature) 30 | Feature(id uint) Feature 31 | FeatureByProps(typ model.FeatureTypeEnumType, role model.RoleType) Feature 32 | 33 | Information() *model.NodeManagementDetailedDiscoveryEntityInformationType 34 | Dump(w io.Writer) 35 | } 36 | 37 | var _ Entity = (*EntityImpl)(nil) 38 | 39 | type EntityImpl struct { 40 | Device Device 41 | Address []model.AddressEntityType 42 | Type model.EntityTypeType 43 | Description model.DescriptionType 44 | Parent Entity 45 | Entities []Entity 46 | Features []Feature 47 | ManufacturerData model.DeviceClassificationManufacturerDataType 48 | OperationState model.DeviceDiagnosisOperatingStateType 49 | } 50 | 51 | func (e *EntityImpl) GetAddress() []model.AddressEntityType { 52 | return e.Address 53 | } 54 | 55 | func (e *EntityImpl) SetAddress(addr []model.AddressEntityType) { 56 | e.Address = addr 57 | } 58 | 59 | func (e *EntityImpl) GetDevice() Device { 60 | return e.Device 61 | } 62 | 63 | func (e *EntityImpl) SetDevice(d Device) { 64 | e.Device = d 65 | } 66 | 67 | func (e *EntityImpl) GetFeatures() []Feature { 68 | return e.Features 69 | } 70 | 71 | func (e *EntityImpl) GetType() model.EntityTypeType { 72 | return e.Type 73 | } 74 | 75 | func (e *EntityImpl) GetManufacturerData() model.DeviceClassificationManufacturerDataType { 76 | return e.ManufacturerData 77 | } 78 | 79 | func (e *EntityImpl) SetManufacturerData(data model.DeviceClassificationManufacturerDataType) { 80 | e.ManufacturerData = data 81 | } 82 | 83 | func (e *EntityImpl) GetOperationState() model.DeviceDiagnosisOperatingStateType { 84 | return e.OperationState 85 | } 86 | 87 | func (e *EntityImpl) SetOperationState(data model.DeviceDiagnosisOperatingStateType) { 88 | e.OperationState = data 89 | } 90 | 91 | // TODO maintain feature id when adding 92 | func (e *EntityImpl) Add(f Feature) { 93 | f.SetEntity(e) 94 | e.Features = append(e.Features, f) 95 | } 96 | 97 | func (e *EntityImpl) Feature(id uint) Feature { 98 | if e != nil { 99 | for _, f := range e.Features { 100 | if f.GetID() == id { 101 | return f 102 | } 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | func (e *EntityImpl) FeatureByProps(typ model.FeatureTypeEnumType, role model.RoleType) Feature { 109 | if e != nil { 110 | for _, f := range e.Features { 111 | if f.GetType() == typ && f.GetRole() == role { 112 | return f 113 | } 114 | } 115 | } 116 | return nil 117 | } 118 | 119 | func (e *EntityImpl) Information() *model.NodeManagementDetailedDiscoveryEntityInformationType { 120 | res := model.NodeManagementDetailedDiscoveryEntityInformationType{ 121 | Description: &model.NetworkManagementEntityDescriptionDataType{ 122 | EntityAddress: EntityAddressType(e), 123 | EntityType: &e.Type, 124 | }, 125 | } 126 | 127 | return &res 128 | } 129 | 130 | func (e *EntityImpl) Dump(w io.Writer) { 131 | for _, f := range e.Features { 132 | addr := EntityAddressString(e) 133 | fmt.Fprintf(w, " e[%s] f-%d type=%s.%s\n", addr, f.GetID(), f.GetRole(), f.GetType()) 134 | f.Dump(w) 135 | } 136 | for _, child := range e.Entities { 137 | child.Dump(w) 138 | } 139 | } 140 | 141 | func UnmarshalEntity( 142 | deviceAddress model.AddressDeviceType, 143 | entityData model.NodeManagementDetailedDiscoveryEntityInformationType, 144 | ) *EntityImpl { 145 | var e *EntityImpl 146 | 147 | if eid := entityData.Description; eid != nil { 148 | e = new(EntityImpl) 149 | 150 | if eid.EntityType != nil { 151 | e.Type = *eid.EntityType 152 | } 153 | 154 | if eid.Description != nil { 155 | e.Description = *eid.Description 156 | } 157 | 158 | if ea := eid.EntityAddress; ea != nil { 159 | e.Address = ea.Entity 160 | 161 | if ea.Device != nil && *ea.Device != deviceAddress { 162 | return nil 163 | } 164 | } 165 | } 166 | 167 | return e 168 | } 169 | 170 | func EntityAddressString(e Entity) string { 171 | return strings.Join(lo.Map(e.GetAddress(), func(id model.AddressEntityType, _ int) string { 172 | return strconv.FormatUint(uint64(id), 10) 173 | }), ",") 174 | } 175 | 176 | func EntityAddressType(e Entity) *model.EntityAddressType { 177 | addr := e.GetDevice().GetAddress() 178 | 179 | res := model.EntityAddressType{ 180 | Device: &addr, 181 | Entity: e.GetAddress(), 182 | } 183 | 184 | return &res 185 | } 186 | -------------------------------------------------------------------------------- /spine/feature.go: -------------------------------------------------------------------------------- 1 | package spine 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/evcc-io/eebus/spine/model" 9 | ) 10 | 11 | type ClientFeature interface { 12 | ServerFound(ctrl Context, e Feature) error 13 | } 14 | 15 | type Feature interface { 16 | GetAddress() *model.FeatureAddressType 17 | 18 | GetID() uint 19 | SetID(id uint) 20 | 21 | GetEntity() Entity 22 | SetEntity(e Entity) 23 | 24 | GetType() model.FeatureTypeEnumType 25 | GetRole() model.RoleType 26 | 27 | Add(fun model.FunctionEnumType, r, w bool) 28 | 29 | SupportForFunctionAvailable(fun model.FunctionEnumType) bool 30 | 31 | Information() *model.NodeManagementDetailedDiscoveryFeatureInformationType 32 | Dump(w io.Writer) 33 | 34 | EVDisconnect() 35 | 36 | HandleRequest(ctrl Context, fct model.FunctionEnumType, op model.CmdClassifierType, rf Feature) (*model.MsgCounterType, error) 37 | Handle(ctrl Context, rf model.FeatureAddressType, op model.CmdClassifierType, cmd model.CmdType, isPartialForCmd bool) error 38 | HandleResultData(ctrl Context, op model.CmdClassifierType) error 39 | } 40 | 41 | var _ Feature = (*FeatureImpl)(nil) 42 | 43 | type FeatureImpl struct { 44 | Entity Entity 45 | ID uint 46 | Type model.FeatureTypeEnumType 47 | Description model.DescriptionType 48 | Role model.RoleType 49 | Functions map[model.FunctionEnumType]RW 50 | Subscriptions []model.SubscriptionManagementEntryDataType 51 | } 52 | 53 | func (f *FeatureImpl) GetAddress() *model.FeatureAddressType { 54 | return FeatureAddressType(f) 55 | } 56 | 57 | func (f *FeatureImpl) GetID() uint { 58 | return f.ID 59 | } 60 | 61 | func (f *FeatureImpl) SetID(id uint) { 62 | f.ID = id 63 | } 64 | 65 | func (f *FeatureImpl) GetEntity() Entity { 66 | return f.Entity 67 | } 68 | 69 | func (f *FeatureImpl) SetEntity(e Entity) { 70 | f.Entity = e 71 | } 72 | 73 | func (f *FeatureImpl) GetType() model.FeatureTypeEnumType { 74 | return f.Type 75 | } 76 | 77 | func (f *FeatureImpl) GetRole() model.RoleType { 78 | return f.Role 79 | } 80 | 81 | func (f *FeatureImpl) Add(fun model.FunctionEnumType, r, w bool) { 82 | if f.Functions == nil { 83 | f.Functions = make(map[model.FunctionEnumType]RW) 84 | } 85 | 86 | if f.Role == model.RoleTypeClient { 87 | panic("cannot add functions to client role") 88 | } 89 | 90 | f.Functions[fun] = RW{r, w} 91 | } 92 | 93 | func (f *FeatureImpl) SupportForFunctionAvailable(fun model.FunctionEnumType) bool { 94 | _, found := f.Functions[fun] 95 | return found 96 | } 97 | 98 | func (f *FeatureImpl) Information() *model.NodeManagementDetailedDiscoveryFeatureInformationType { 99 | var funs []model.FunctionPropertyType 100 | for fun, rw := range f.Functions { 101 | var functionType model.FunctionType = model.FunctionType(fun) 102 | sf := model.FunctionPropertyType{ 103 | Function: &functionType, 104 | PossibleOperations: rw.Information(), 105 | } 106 | 107 | funs = append(funs, sf) 108 | } 109 | 110 | var featureType model.FeatureTypeType = model.FeatureTypeType(f.Type) 111 | var featureRole model.RoleType = model.RoleType(f.Role) 112 | 113 | res := model.NodeManagementDetailedDiscoveryFeatureInformationType{ 114 | Description: &model.NetworkManagementFeatureDescriptionDataType{ 115 | FeatureAddress: FeatureAddressType(f), 116 | FeatureType: &featureType, 117 | Role: &featureRole, 118 | SupportedFunction: funs, 119 | }, 120 | } 121 | 122 | return &res 123 | } 124 | 125 | func (f *FeatureImpl) Dump(w io.Writer) { 126 | for fun, ops := range f.Functions { 127 | fmt.Fprintf(w, " {%s} %s\n", ops, fun) 128 | } 129 | } 130 | 131 | func (f *FeatureImpl) EVDisconnect() {} 132 | 133 | func (f *FeatureImpl) HandleRequest(ctrl Context, fct model.FunctionEnumType, op model.CmdClassifierType, rf Feature) (*model.MsgCounterType, error) { 134 | return nil, errors.New("HandleRequest() not implemented") 135 | } 136 | 137 | func (f *FeatureImpl) Handle(ctrl Context, rf model.FeatureAddressType, op model.CmdClassifierType, cmd model.CmdType, isPartialForCmd bool) error { 138 | return errors.New("Handle() not implemented") 139 | } 140 | 141 | func (f *FeatureImpl) HandleResultData(ctrl Context, op model.CmdClassifierType) error { 142 | switch op { 143 | case model.CmdClassifierTypeResult: 144 | // TODO process the return result data for the message sent with the ID in msgCounterReference 145 | // error numbers explained in Resource Spec 3.11 146 | return nil 147 | 148 | default: 149 | return fmt.Errorf("ResultData CmdClassifierType %s not implemented", op) 150 | } 151 | } 152 | 153 | func UnmarshalFeature( 154 | // entityID uint, 155 | featureData model.NodeManagementDetailedDiscoveryFeatureInformationType, 156 | ) *FeatureImpl { 157 | var f *FeatureImpl 158 | 159 | if fid := featureData.Description; fid != nil { 160 | f = &FeatureImpl{ 161 | Type: model.FeatureTypeEnumType(*fid.FeatureType), 162 | } 163 | 164 | if fid.Description != nil { 165 | f.Description = *fid.Description 166 | } 167 | 168 | if fid.Role != nil { 169 | f.Role = *fid.Role 170 | } 171 | 172 | if addr := fid.FeatureAddress; addr != nil { 173 | f.ID = uint(*addr.Feature) 174 | } 175 | 176 | for _, sf := range fid.SupportedFunction { 177 | f.Add(model.FunctionEnumType(*sf.Function), sf.PossibleOperations.Read != nil, sf.PossibleOperations.Write != nil) 178 | } 179 | } 180 | 181 | return f 182 | } 183 | 184 | func FeatureAddressType(f Feature) *model.FeatureAddressType { 185 | e := f.GetEntity() 186 | featureId := model.AddressFeatureType(f.GetID()) 187 | 188 | res := model.FeatureAddressType{ 189 | Entity: e.GetAddress(), 190 | Feature: &featureId, 191 | } 192 | 193 | device := e.GetDevice() 194 | if device != nil { 195 | addr := device.GetAddress() 196 | res.Device = &addr 197 | } 198 | 199 | return &res 200 | } 201 | -------------------------------------------------------------------------------- /spine/model/bindingmanagement.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // BindingIdType type 9 | type BindingIdType uint 10 | 11 | // BindingManagementEntryDataType complex type 12 | type BindingManagementEntryDataType struct { 13 | BindingId *BindingIdType `json:"bindingId,omitempty"` 14 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 15 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 16 | Label *LabelType `json:"label,omitempty"` 17 | Description *DescriptionType `json:"description,omitempty"` 18 | } 19 | 20 | // MarshalJSON is the SHIP serialization marshaller 21 | func (m BindingManagementEntryDataType) MarshalJSON() ([]byte, error) { 22 | return util.Marshal(m) 23 | } 24 | 25 | // UnmarshalJSON is the SHIP serialization unmarshaller 26 | func (m *BindingManagementEntryDataType) UnmarshalJSON(data []byte) error { 27 | return util.Unmarshal(data, &m) 28 | } 29 | 30 | // BindingManagementEntryListDataType complex type 31 | type BindingManagementEntryListDataType struct { 32 | BindingManagementEntryData []BindingManagementEntryDataType `json:"bindingManagementEntryData,omitempty"` 33 | } 34 | 35 | // MarshalJSON is the SHIP serialization marshaller 36 | func (m BindingManagementEntryListDataType) MarshalJSON() ([]byte, error) { 37 | return util.Marshal(m) 38 | } 39 | 40 | // UnmarshalJSON is the SHIP serialization unmarshaller 41 | func (m *BindingManagementEntryListDataType) UnmarshalJSON(data []byte) error { 42 | return util.Unmarshal(data, &m) 43 | } 44 | 45 | // BindingManagementEntryListDataSelectorsType complex type 46 | type BindingManagementEntryListDataSelectorsType struct { 47 | BindingId *BindingIdType `json:"bindingId,omitempty"` 48 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 49 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 50 | } 51 | 52 | // MarshalJSON is the SHIP serialization marshaller 53 | func (m BindingManagementEntryListDataSelectorsType) MarshalJSON() ([]byte, error) { 54 | return util.Marshal(m) 55 | } 56 | 57 | // UnmarshalJSON is the SHIP serialization unmarshaller 58 | func (m *BindingManagementEntryListDataSelectorsType) UnmarshalJSON(data []byte) error { 59 | return util.Unmarshal(data, &m) 60 | } 61 | 62 | // BindingManagementRequestCallType complex type 63 | type BindingManagementRequestCallType struct { 64 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 65 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 66 | ServerFeatureType *FeatureTypeType `json:"serverFeatureType,omitempty"` 67 | } 68 | 69 | // MarshalJSON is the SHIP serialization marshaller 70 | func (m BindingManagementRequestCallType) MarshalJSON() ([]byte, error) { 71 | return util.Marshal(m) 72 | } 73 | 74 | // UnmarshalJSON is the SHIP serialization unmarshaller 75 | func (m *BindingManagementRequestCallType) UnmarshalJSON(data []byte) error { 76 | return util.Unmarshal(data, &m) 77 | } 78 | 79 | // BindingManagementDeleteCallType complex type 80 | type BindingManagementDeleteCallType struct { 81 | BindingId *BindingIdType `json:"bindingId,omitempty"` 82 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 83 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 84 | } 85 | 86 | // MarshalJSON is the SHIP serialization marshaller 87 | func (m BindingManagementDeleteCallType) MarshalJSON() ([]byte, error) { 88 | return util.Marshal(m) 89 | } 90 | 91 | // UnmarshalJSON is the SHIP serialization unmarshaller 92 | func (m *BindingManagementDeleteCallType) UnmarshalJSON(data []byte) error { 93 | return util.Unmarshal(data, &m) 94 | } 95 | -------------------------------------------------------------------------------- /spine/model/commandframe.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // MsgCounterType type 9 | type MsgCounterType uint64 10 | 11 | // CmdClassifierType type 12 | type CmdClassifierType string 13 | 14 | // CmdClassifierType constants 15 | const ( 16 | CmdClassifierTypeRead CmdClassifierType = "read" 17 | CmdClassifierTypeReply CmdClassifierType = "reply" 18 | CmdClassifierTypeNotify CmdClassifierType = "notify" 19 | CmdClassifierTypeWrite CmdClassifierType = "write" 20 | CmdClassifierTypeCall CmdClassifierType = "call" 21 | CmdClassifierTypeResult CmdClassifierType = "result" 22 | ) 23 | 24 | // FilterIdType type 25 | type FilterIdType uint 26 | 27 | // FilterType complex type 28 | type FilterType struct { 29 | FilterId *FilterIdType `json:"filterId,omitempty"` 30 | CmdControl *CmdControlType `json:"cmdControl,omitempty"` 31 | 32 | // DataSelectorsChoiceGroup 33 | ElectricalConnectionPermittedValueSetListDataSelectors *ElectricalConnectionPermittedValueSetListDataSelectorsType `json:"electricalConnectionPermittedValueSetListDataSelectors,omitempty"` 34 | ElectricalConnectionDescriptionListDataSelectors *ElectricalConnectionDescriptionListDataSelectorsType `json:"electricalConnectionDescriptionListDataSelectors,omitempty"` 35 | ElectricalConnectionParameterDescriptionListDataSelectors *ElectricalConnectionParameterDescriptionListDataSelectorsType `json:"electricalConnectionParameterDescriptionListDataSelectors,omitempty"` 36 | MeasurementDescriptionListDataSelectors *MeasurementDescriptionListDataSelectorsType `json:"measurementDescriptionListDataSelectors,omitempty"` 37 | MeasurementListDataSelectors *MeasurementListDataSelectorsType `json:"measurementListDataSelectors,omitempty"` 38 | } 39 | 40 | // MarshalJSON is the SHIP serialization marshaller 41 | func (m FilterType) MarshalJSON() ([]byte, error) { 42 | return util.Marshal(m) 43 | } 44 | 45 | // UnmarshalJSON is the SHIP serialization unmarshaller 46 | func (m *FilterType) UnmarshalJSON(data []byte) error { 47 | return util.Unmarshal(data, &m) 48 | } 49 | 50 | // CmdControlType complex type 51 | type CmdControlType struct { 52 | Delete *ElementTagType `json:"delete,omitempty"` 53 | Partial *ElementTagType `json:"partial,omitempty"` 54 | } 55 | 56 | // MarshalJSON is the SHIP serialization marshaller 57 | func (m CmdControlType) MarshalJSON() ([]byte, error) { 58 | return util.Marshal(m) 59 | } 60 | 61 | // UnmarshalJSON is the SHIP serialization unmarshaller 62 | func (m *CmdControlType) UnmarshalJSON(data []byte) error { 63 | return util.Unmarshal(data, &m) 64 | } 65 | 66 | // Filter element 67 | type Filter struct { 68 | FilterId *FilterIdType `json:"filterId,omitempty"` 69 | CmdControl *CmdControlType `json:"cmdControl,omitempty"` 70 | } 71 | 72 | // MarshalJSON is the SHIP serialization marshaller 73 | func (m Filter) MarshalJSON() ([]byte, error) { 74 | return util.Marshal(m) 75 | } 76 | 77 | // UnmarshalJSON is the SHIP serialization unmarshaller 78 | func (m *Filter) UnmarshalJSON(data []byte) error { 79 | return util.Unmarshal(data, &m) 80 | } 81 | 82 | // CmdControl element 83 | type CmdControl struct { 84 | Delete *ElementTagType `json:"delete,omitempty"` 85 | Partial *ElementTagType `json:"partial,omitempty"` 86 | } 87 | 88 | // MarshalJSON is the SHIP serialization marshaller 89 | func (m CmdControl) MarshalJSON() ([]byte, error) { 90 | return util.Marshal(m) 91 | } 92 | 93 | // UnmarshalJSON is the SHIP serialization unmarshaller 94 | func (m *CmdControl) UnmarshalJSON(data []byte) error { 95 | return util.Unmarshal(data, &m) 96 | } 97 | 98 | // CmdType complex type 99 | type CmdType struct { 100 | // CmdOptionGroup 101 | Function *FunctionType `json:"function,omitempty"` 102 | Filter []FilterType `json:"filter,omitempty"` 103 | 104 | // DataChoiceGroup 105 | DeviceClassificationManufacturerData *DeviceClassificationManufacturerDataType `json:"deviceClassificationManufacturerData,omitempty"` 106 | DeviceConfigurationKeyValueDescriptionListData *DeviceConfigurationKeyValueDescriptionListDataType `json:"deviceConfigurationKeyValueDescriptionListData,omitempty"` 107 | DeviceConfigurationKeyValueListData *DeviceConfigurationKeyValueListDataType `json:"deviceConfigurationKeyValueListData,omitempty"` 108 | DeviceDiagnosisHeartbeatData *DeviceDiagnosisHeartbeatDataType `json:"deviceDiagnosisHeartbeatData,omitempty"` 109 | DeviceDiagnosisStateData *DeviceDiagnosisStateDataType `json:"deviceDiagnosisStateData,omitempty"` 110 | ElectricalConnectionDescriptionListData *ElectricalConnectionDescriptionListDataType `json:"electricalConnectionDescriptionListData,omitempty"` 111 | ElectricalConnectionParameterDescriptionListData *ElectricalConnectionParameterDescriptionListDataType `json:"electricalConnectionParameterDescriptionListData,omitempty"` 112 | ElectricalConnectionPermittedValueSetListData *ElectricalConnectionPermittedValueSetListDataType `json:"electricalConnectionPermittedValueSetListData,omitempty"` 113 | IdentificationListData *IdentificationListDataType `json:"identificationListData,omitempty"` 114 | IncentiveTableDescriptionData *IncentiveTableDescriptionDataType `json:"incentiveTableDescriptionData,omitempty"` 115 | IncentiveTableConstraintsData *IncentiveTableConstraintsDataType `json:"incentiveTableConstraintsData,omitempty"` 116 | IncentiveTableData *IncentiveTableDataType `json:"incentiveTableData,omitempty"` 117 | LoadControlLimitDescriptionListData *LoadControlLimitDescriptionListDataType `json:"loadControlLimitDescriptionListData,omitempty"` 118 | LoadControlLimitListData *LoadControlLimitListDataType `json:"loadControlLimitListData,omitempty"` 119 | NodeManagementBindingRequestCall *NodeManagementBindingRequestCallType `json:"nodeManagementBindingRequestCall,omitempty"` 120 | NodeManagementDestinationListData *NodeManagementDestinationListDataType `json:"nodeManagementDestinationListData,omitempty"` 121 | NodeManagementDetailedDiscoveryData *NodeManagementDetailedDiscoveryDataType `json:"nodeManagementDetailedDiscoveryData,omitempty"` 122 | NodeManagementSubscriptionData *NodeManagementSubscriptionDataType `json:"nodeManagementSubscriptionData,omitempty"` 123 | NodeManagementSubscriptionRequestCall *NodeManagementSubscriptionRequestCallType `json:"nodeManagementSubscriptionRequestCall,omitempty"` 124 | NodeManagementSubscriptionDeleteCall *NodeManagementSubscriptionDeleteCallType `json:"nodeManagementSubscriptionDeleteCall,omitempty"` 125 | NodeManagementUseCaseData *NodeManagementUseCaseDataType `json:"nodeManagementUseCaseData,omitempty"` 126 | MeasurementConstraintsListData *MeasurementConstraintsListDataType `json:"measurementConstraintsListData,omitempty"` 127 | MeasurementDescriptionListData *MeasurementDescriptionListDataType `json:"measurementDescriptionListData,omitempty"` 128 | MeasurementListData *MeasurementListDataType `json:"measurementListData,omitempty"` 129 | ResultData *ResultDataType `json:"resultData,omitempty"` 130 | TimeSeriesConstraintsData *TimeSeriesConstraintsDataType `json:"timeSeriesConstraintsData,omitempty"` 131 | TimeSeriesConstraintsListData *TimeSeriesConstraintsListDataType `json:"timeSeriesConstraintsListData,omitempty"` 132 | TimeSeriesDescriptionListData *TimeSeriesDescriptionListDataType `json:"timeSeriesDescriptionListData,omitempty"` 133 | TimeSeriesListData *TimeSeriesListDataType `json:"timeSeriesListData,omitempty"` 134 | 135 | // DataExtendGroup 136 | } 137 | 138 | // MarshalJSON is the SHIP serialization marshaller 139 | func (m CmdType) MarshalJSON() ([]byte, error) { 140 | return util.Marshal(m) 141 | } 142 | 143 | // UnmarshalJSON is the SHIP serialization unmarshaller 144 | func (m *CmdType) UnmarshalJSON(data []byte) error { 145 | return util.Unmarshal(data, &m) 146 | } 147 | -------------------------------------------------------------------------------- /spine/model/commondatatypes_additions.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/rickb777/date/period" 10 | ) 11 | 12 | func (m ScaledNumberType) GetValue() float64 { 13 | if m.Number == nil { 14 | return 0 15 | } 16 | var scale float64 = 0 17 | if m.Scale != nil { 18 | scale = float64(*m.Scale) 19 | } 20 | return float64(*m.Number) * math.Pow(10, scale) 21 | } 22 | 23 | func NewScaledNumberType(value float64) *ScaledNumberType { 24 | m := &ScaledNumberType{} 25 | 26 | numberOfDecimals := 0 27 | temp := strconv.FormatFloat(value, 'f', -1, 64) 28 | index := strings.IndexByte(temp, '.') 29 | if index > -1 { 30 | numberOfDecimals = len(temp) - index - 1 31 | } 32 | 33 | if numberOfDecimals > 4 { 34 | numberOfDecimals = 4 35 | } 36 | 37 | numberValue := NumberType(math.Trunc(value * math.Pow(10, float64(numberOfDecimals)))) 38 | m.Number = &numberValue 39 | 40 | if numberValue != 0 { 41 | scaleValue := ScaleType(-numberOfDecimals) 42 | m.Scale = &scaleValue 43 | } 44 | 45 | return m 46 | } 47 | 48 | func NewISO8601Duration(duration time.Duration) *string { 49 | d, _ := period.NewOf(duration) 50 | value := d.String() 51 | return &value 52 | } 53 | -------------------------------------------------------------------------------- /spine/model/datagram.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // CmiDatagramType message container 9 | type CmiDatagramType struct { 10 | Datagram DatagramType `json:"datagram"` 11 | } 12 | 13 | // DatagramType complex type 14 | type DatagramType struct { 15 | Header HeaderType `json:"header"` 16 | Payload PayloadType `json:"payload"` 17 | } 18 | 19 | // MarshalJSON is the SHIP serialization marshaller 20 | func (m DatagramType) MarshalJSON() ([]byte, error) { 21 | return util.Marshal(m) 22 | } 23 | 24 | // UnmarshalJSON is the SHIP serialization unmarshaller 25 | func (m *DatagramType) UnmarshalJSON(data []byte) error { 26 | return util.Unmarshal(data, &m) 27 | } 28 | 29 | // HeaderType complex type 30 | type HeaderType struct { 31 | SpecificationVersion *SpecificationVersionType `json:"specificationVersion,omitempty"` 32 | AddressSource *FeatureAddressType `json:"addressSource,omitempty"` 33 | AddressDestination *FeatureAddressType `json:"addressDestination,omitempty"` 34 | AddressOriginator *FeatureAddressType `json:"addressOriginator,omitempty"` 35 | MsgCounter *MsgCounterType `json:"msgCounter,omitempty"` 36 | MsgCounterReference *MsgCounterType `json:"msgCounterReference,omitempty"` 37 | CmdClassifier *CmdClassifierType `json:"cmdClassifier,omitempty"` 38 | AckRequest *bool `json:"ackRequest,omitempty"` 39 | Timestamp *string `json:"timestamp,omitempty"` 40 | } 41 | 42 | // MarshalJSON is the SHIP serialization marshaller 43 | func (m HeaderType) MarshalJSON() ([]byte, error) { 44 | return util.Marshal(m) 45 | } 46 | 47 | // UnmarshalJSON is the SHIP serialization unmarshaller 48 | func (m *HeaderType) UnmarshalJSON(data []byte) error { 49 | return util.Unmarshal(data, &m) 50 | } 51 | 52 | // PayloadType complex type 53 | type PayloadType struct { 54 | Cmd []CmdType `json:"cmd"` 55 | } 56 | 57 | // MarshalJSON is the SHIP serialization marshaller 58 | func (m PayloadType) MarshalJSON() ([]byte, error) { 59 | return util.Marshal(m) 60 | } 61 | 62 | // UnmarshalJSON is the SHIP serialization unmarshaller 63 | func (m *PayloadType) UnmarshalJSON(data []byte) error { 64 | return util.Unmarshal(data, &m) 65 | } 66 | -------------------------------------------------------------------------------- /spine/model/deviceclassification.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // DeviceClassificationStringType type 9 | type DeviceClassificationStringType string 10 | 11 | // PowerSourceType type 12 | type PowerSourceType PowerSourceEnumType 13 | 14 | // PowerSourceEnumType type 15 | type PowerSourceEnumType string 16 | 17 | // PowerSourceEnumType constants 18 | const ( 19 | PowerSourceEnumTypeUnknown PowerSourceEnumType = "unknown" 20 | PowerSourceEnumTypeMainssinglephase PowerSourceEnumType = "mainsSinglePhase" 21 | PowerSourceEnumTypeMains3Phase PowerSourceEnumType = "mains3Phase" 22 | PowerSourceEnumTypeBattery PowerSourceEnumType = "battery" 23 | PowerSourceEnumTypeDc PowerSourceEnumType = "dc" 24 | ) 25 | 26 | // DeviceClassificationManufacturerDataType complex type 27 | type DeviceClassificationManufacturerDataType struct { 28 | DeviceName *DeviceClassificationStringType `json:"deviceName,omitempty"` 29 | DeviceCode *DeviceClassificationStringType `json:"deviceCode,omitempty"` 30 | SerialNumber *DeviceClassificationStringType `json:"serialNumber,omitempty"` 31 | SoftwareRevision *DeviceClassificationStringType `json:"softwareRevision,omitempty"` 32 | HardwareRevision *DeviceClassificationStringType `json:"hardwareRevision,omitempty"` 33 | VendorName *DeviceClassificationStringType `json:"vendorName,omitempty"` 34 | VendorCode *DeviceClassificationStringType `json:"vendorCode,omitempty"` 35 | BrandName *DeviceClassificationStringType `json:"brandName,omitempty"` 36 | PowerSource string `json:"powerSource,omitempty"` 37 | ManufacturerNodeIdentification *DeviceClassificationStringType `json:"manufacturerNodeIdentification,omitempty"` 38 | ManufacturerLabel *LabelType `json:"manufacturerLabel,omitempty"` 39 | ManufacturerDescription *DescriptionType `json:"manufacturerDescription,omitempty"` 40 | } 41 | 42 | // MarshalJSON is the SHIP serialization marshaller 43 | func (m DeviceClassificationManufacturerDataType) MarshalJSON() ([]byte, error) { 44 | return util.Marshal(m) 45 | } 46 | 47 | // UnmarshalJSON is the SHIP serialization unmarshaller 48 | func (m *DeviceClassificationManufacturerDataType) UnmarshalJSON(data []byte) error { 49 | return util.Unmarshal(data, &m) 50 | } 51 | 52 | // DeviceClassificationUserDataType complex type 53 | type DeviceClassificationUserDataType struct { 54 | UserNodeIdentification *DeviceClassificationStringType `json:"userNodeIdentification,omitempty"` 55 | UserLabel *LabelType `json:"userLabel,omitempty"` 56 | UserDescription *DescriptionType `json:"userDescription,omitempty"` 57 | } 58 | 59 | // MarshalJSON is the SHIP serialization marshaller 60 | func (m DeviceClassificationUserDataType) MarshalJSON() ([]byte, error) { 61 | return util.Marshal(m) 62 | } 63 | 64 | // UnmarshalJSON is the SHIP serialization unmarshaller 65 | func (m *DeviceClassificationUserDataType) UnmarshalJSON(data []byte) error { 66 | return util.Unmarshal(data, &m) 67 | } 68 | -------------------------------------------------------------------------------- /spine/model/deviceclassification_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestDeviceClassificationManufacturerData(t *testing.T) { 10 | var specificationVersion SpecificationVersionType = "1.2.0" 11 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 12 | var deviceEvse AddressDeviceType = "d:_i:3210_EVSE" 13 | var addressSourceFeature AddressFeatureType = 6 14 | var addressSoureEntity1 AddressEntityType = 1 15 | var addressDestinationFeature AddressFeatureType = 1 16 | var addressDestinationEntity1 AddressEntityType = 1 17 | var msgCounter MsgCounterType = 194 18 | var msgCounterReference MsgCounterType = 4890 19 | var cmdClassifier CmdClassifierType = CmdClassifierTypeReply 20 | 21 | var deviceName DeviceClassificationStringType = "" 22 | var deviceCode DeviceClassificationStringType = "" 23 | var brandName DeviceClassificationStringType = "" 24 | 25 | datagram := CmiDatagramType{ 26 | Datagram: DatagramType{ 27 | Header: HeaderType{ 28 | SpecificationVersion: &specificationVersion, 29 | AddressSource: &FeatureAddressType{ 30 | Feature: &addressSourceFeature, 31 | Entity: []AddressEntityType{addressSoureEntity1, addressSoureEntity1}, 32 | Device: &deviceEvse, 33 | }, 34 | AddressDestination: &FeatureAddressType{ 35 | Feature: &addressDestinationFeature, 36 | Entity: []AddressEntityType{addressDestinationEntity1}, 37 | Device: &deviceHems, 38 | }, 39 | MsgCounter: &msgCounter, 40 | MsgCounterReference: &msgCounterReference, 41 | CmdClassifier: &cmdClassifier, 42 | }, 43 | Payload: PayloadType{ 44 | Cmd: []CmdType{{ 45 | DeviceClassificationManufacturerData: &DeviceClassificationManufacturerDataType{ 46 | DeviceName: &deviceName, 47 | DeviceCode: &deviceCode, 48 | BrandName: &brandName, 49 | PowerSource: string(PowerSourceEnumTypeMains3Phase), 50 | }, 51 | }}, 52 | }, 53 | }, 54 | } 55 | json, err := json.Marshal(datagram) 56 | if err != nil { 57 | t.Errorf("TestDeviceClassificationManufacturerData() error = %v", err) 58 | } 59 | jsonString := string(json) 60 | 61 | jsonTest := `{"datagram":[{"header":[{"specificationVersion":"1.2.0"},{"addressSource":[{"device":"d:_i:3210_EVSE"},{"entity":[1,1]},{"feature":6}]},{"addressDestination":[{"device":"d:_i:3210_HEMS"},{"entity":[1]},{"feature":1}]},{"msgCounter":194},{"msgCounterReference":4890},{"cmdClassifier":"reply"}]},{"payload":[{"cmd":[[{"deviceClassificationManufacturerData":[{"deviceName":""},{"deviceCode":""},{"brandName":""},{"powerSource":"mains3Phase"}]}]]}]}]}` 62 | if jsonString != jsonTest { 63 | fmt.Println("EXPECTED:") 64 | fmt.Println(string(jsonTest)) 65 | fmt.Println("\nACTUAL:") 66 | fmt.Println(string(jsonString)) 67 | 68 | t.Errorf("TestDeviceClassificationManufacturerData() actual json string doesn't match expected result") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /spine/model/deviceconfiguration_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestDeviceConfigurationKeyValueDescriptionListData(t *testing.T) { 10 | var specificationVersion SpecificationVersionType = "1.2.0" 11 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 12 | var deviceEvse AddressDeviceType = "d:_i:3210_EVSE" 13 | var addressSourceFeature AddressFeatureType = 5 14 | var addressSoureEntity1 AddressEntityType = 1 15 | var addressDestinationFeature AddressFeatureType = 4 16 | var addressDestinationEntity1 AddressEntityType = 1 17 | var msgCounter MsgCounterType = 237 18 | var msgCounterReference MsgCounterType = 4926 19 | var cmdClassifier CmdClassifierType = CmdClassifierTypeReply 20 | 21 | var keyID1 DeviceConfigurationKeyIdType = 1 22 | var keyName1 string = string(DeviceConfigurationKeyNameEnumTypeAsymmetricChargingSupported) 23 | var keyID2 DeviceConfigurationKeyIdType = 2 24 | var keyName2 string = string(DeviceConfigurationKeyNameEnumTypeCommunicationsStandard) 25 | var typeBool DeviceConfigurationKeyValueTypeType = DeviceConfigurationKeyValueTypeTypeBoolean 26 | var typeString DeviceConfigurationKeyValueTypeType = DeviceConfigurationKeyValueTypeTypeString 27 | 28 | datagram := CmiDatagramType{ 29 | Datagram: DatagramType{ 30 | Header: HeaderType{ 31 | SpecificationVersion: &specificationVersion, 32 | AddressSource: &FeatureAddressType{ 33 | Feature: &addressSourceFeature, 34 | Entity: []AddressEntityType{addressSoureEntity1, addressSoureEntity1}, 35 | Device: &deviceEvse, 36 | }, 37 | AddressDestination: &FeatureAddressType{ 38 | Feature: &addressDestinationFeature, 39 | Entity: []AddressEntityType{addressDestinationEntity1}, 40 | Device: &deviceHems, 41 | }, 42 | MsgCounter: &msgCounter, 43 | MsgCounterReference: &msgCounterReference, 44 | CmdClassifier: &cmdClassifier, 45 | }, 46 | Payload: PayloadType{ 47 | Cmd: []CmdType{{ 48 | DeviceConfigurationKeyValueDescriptionListData: &DeviceConfigurationKeyValueDescriptionListDataType{ 49 | DeviceConfigurationKeyValueDescriptionData: []DeviceConfigurationKeyValueDescriptionDataType{ 50 | { 51 | KeyId: &keyID1, 52 | KeyName: &keyName1, 53 | ValueType: &typeBool, 54 | }, 55 | { 56 | KeyId: &keyID2, 57 | KeyName: &keyName2, 58 | ValueType: &typeString, 59 | }, 60 | }, 61 | }, 62 | }}, 63 | }, 64 | }, 65 | } 66 | json, err := json.Marshal(datagram) 67 | if err != nil { 68 | t.Errorf("TestDeviceConfigurationKeyValueDescriptionListData() error = %v", err) 69 | } 70 | jsonString := string(json) 71 | 72 | jsonTest := `{"datagram":[{"header":[{"specificationVersion":"1.2.0"},{"addressSource":[{"device":"d:_i:3210_EVSE"},{"entity":[1,1]},{"feature":5}]},{"addressDestination":[{"device":"d:_i:3210_HEMS"},{"entity":[1]},{"feature":4}]},{"msgCounter":237},{"msgCounterReference":4926},{"cmdClassifier":"reply"}]},{"payload":[{"cmd":[[{"deviceConfigurationKeyValueDescriptionListData":[{"deviceConfigurationKeyValueDescriptionData":[[{"keyId":1},{"keyName":"asymmetricChargingSupported"},{"valueType":"boolean"}],[{"keyId":2},{"keyName":"communicationsStandard"},{"valueType":"string"}]]}]}]]}]}]}` 73 | if jsonString != jsonTest { 74 | fmt.Println("EXPECTED:") 75 | fmt.Println(string(jsonTest)) 76 | fmt.Println("\nACTUAL:") 77 | fmt.Println(string(jsonString)) 78 | 79 | t.Errorf("TestDeviceConfigurationKeyValueDescriptionListData() actual json string doesn't match expected result") 80 | } 81 | } 82 | 83 | func TestDeviceConfigurationKeyValueListData(t *testing.T) { 84 | var specificationVersion SpecificationVersionType = "1.2.0" 85 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 86 | var deviceEvse AddressDeviceType = "d:_i:3210_EVSE" 87 | var addressSourceFeature AddressFeatureType = 5 88 | var addressSoureEntity1 AddressEntityType = 1 89 | var addressDestinationFeature AddressFeatureType = 4 90 | var addressDestinationEntity1 AddressEntityType = 1 91 | var msgCounter MsgCounterType = 241 92 | var msgCounterReference MsgCounterType = 4931 93 | var cmdClassifier CmdClassifierType = CmdClassifierTypeReply 94 | 95 | var keyID1 DeviceConfigurationKeyIdType = 1 96 | var keyID2 DeviceConfigurationKeyIdType = 2 97 | var keyID1Value bool = false 98 | var keyID2Value DeviceConfigurationKeyValueStringType = "iec61851" 99 | 100 | datagram := CmiDatagramType{ 101 | Datagram: DatagramType{ 102 | Header: HeaderType{ 103 | SpecificationVersion: &specificationVersion, 104 | AddressSource: &FeatureAddressType{ 105 | Feature: &addressSourceFeature, 106 | Entity: []AddressEntityType{addressSoureEntity1, addressSoureEntity1}, 107 | Device: &deviceEvse, 108 | }, 109 | AddressDestination: &FeatureAddressType{ 110 | Feature: &addressDestinationFeature, 111 | Entity: []AddressEntityType{addressDestinationEntity1}, 112 | Device: &deviceHems, 113 | }, 114 | MsgCounter: &msgCounter, 115 | MsgCounterReference: &msgCounterReference, 116 | CmdClassifier: &cmdClassifier, 117 | }, 118 | Payload: PayloadType{ 119 | Cmd: []CmdType{{ 120 | DeviceConfigurationKeyValueListData: &DeviceConfigurationKeyValueListDataType{ 121 | DeviceConfigurationKeyValueData: []DeviceConfigurationKeyValueDataType{ 122 | { 123 | KeyId: &keyID1, 124 | Value: &DeviceConfigurationKeyValueValueType{ 125 | Boolean: &keyID1Value, 126 | }, 127 | }, 128 | { 129 | KeyId: &keyID2, 130 | Value: &DeviceConfigurationKeyValueValueType{ 131 | String: &keyID2Value, 132 | }, 133 | }, 134 | }, 135 | }, 136 | }}, 137 | }, 138 | }, 139 | } 140 | json, err := json.Marshal(datagram) 141 | if err != nil { 142 | t.Errorf("TestDeviceConfigurationKeyValueListData() error = %v", err) 143 | } 144 | jsonString := string(json) 145 | 146 | jsonTest := `{"datagram":[{"header":[{"specificationVersion":"1.2.0"},{"addressSource":[{"device":"d:_i:3210_EVSE"},{"entity":[1,1]},{"feature":5}]},{"addressDestination":[{"device":"d:_i:3210_HEMS"},{"entity":[1]},{"feature":4}]},{"msgCounter":241},{"msgCounterReference":4931},{"cmdClassifier":"reply"}]},{"payload":[{"cmd":[[{"deviceConfigurationKeyValueListData":[{"deviceConfigurationKeyValueData":[[{"keyId":1},{"value":[{"boolean":false}]}],[{"keyId":2},{"value":[{"string":"iec61851"}]}]]}]}]]}]}]}` 147 | if jsonString != jsonTest { 148 | fmt.Println("EXPECTED:") 149 | fmt.Println(string(jsonTest)) 150 | fmt.Println("\nACTUAL:") 151 | fmt.Println(string(jsonString)) 152 | 153 | t.Errorf("TestDeviceConfigurationKeyValueListData() actual json string doesn't match expected result") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /spine/model/devicediagnosis.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // VendorStateCodeType type 9 | type VendorStateCodeType string 10 | 11 | // LastErrorCodeType type 12 | type LastErrorCodeType string 13 | 14 | // DeviceDiagnosisOperatingStateType type 15 | type DeviceDiagnosisOperatingStateType DeviceDiagnosisOperatingStateEnumType 16 | 17 | // DeviceDiagnosisOperatingStateEnumType type 18 | type DeviceDiagnosisOperatingStateEnumType string 19 | 20 | // DeviceDiagnosisOperatingStateEnumType constants 21 | const ( 22 | DeviceDiagnosisOperatingStateEnumTypeNormalOperation DeviceDiagnosisOperatingStateEnumType = "normalOperation" 23 | DeviceDiagnosisOperatingStateEnumTypeStandby DeviceDiagnosisOperatingStateEnumType = "standby" 24 | DeviceDiagnosisOperatingStateEnumTypeFailure DeviceDiagnosisOperatingStateEnumType = "failure" 25 | DeviceDiagnosisOperatingStateEnumTypeServiceNeeded DeviceDiagnosisOperatingStateEnumType = "serviceNeeded" 26 | DeviceDiagnosisOperatingStateEnumTypeOverrideDetected DeviceDiagnosisOperatingStateEnumType = "overrideDetected" 27 | DeviceDiagnosisOperatingStateEnumTypeInAlarm DeviceDiagnosisOperatingStateEnumType = "inAlarm" 28 | DeviceDiagnosisOperatingStateEnumTypeNotReachable DeviceDiagnosisOperatingStateEnumType = "notReachable" 29 | DeviceDiagnosisOperatingStateEnumTypeFinished DeviceDiagnosisOperatingStateEnumType = "finished" 30 | ) 31 | 32 | // PowerSupplyConditionType type 33 | type PowerSupplyConditionType PowerSupplyConditionEnumType 34 | 35 | // PowerSupplyConditionEnumType type 36 | type PowerSupplyConditionEnumType string 37 | 38 | // PowerSupplyConditionEnumType constants 39 | const ( 40 | PowerSupplyConditionEnumTypeGood PowerSupplyConditionEnumType = "good" 41 | PowerSupplyConditionEnumTypeLow PowerSupplyConditionEnumType = "low" 42 | PowerSupplyConditionEnumTypeCritical PowerSupplyConditionEnumType = "critical" 43 | PowerSupplyConditionEnumTypeUnknown PowerSupplyConditionEnumType = "unknown" 44 | PowerSupplyConditionEnumTypeError PowerSupplyConditionEnumType = "error" 45 | ) 46 | 47 | // DeviceDiagnosisStateDataType complex type 48 | type DeviceDiagnosisStateDataType struct { 49 | Timestamp *string `json:"timestamp,omitempty"` 50 | OperatingState *DeviceDiagnosisOperatingStateType `json:"operatingState,omitempty"` 51 | VendorStateCode *VendorStateCodeType `json:"vendorStateCode,omitempty"` 52 | LastErrorCode *LastErrorCodeType `json:"lastErrorCode,omitempty"` 53 | UpTime *string `json:"upTime,omitempty"` 54 | TotalUpTime *string `json:"totalUpTime,omitempty"` 55 | PowerSupplyCondition *PowerSupplyConditionType `json:"powerSupplyCondition,omitempty"` 56 | } 57 | 58 | // MarshalJSON is the SHIP serialization marshaller 59 | func (m DeviceDiagnosisStateDataType) MarshalJSON() ([]byte, error) { 60 | return util.Marshal(m) 61 | } 62 | 63 | // UnmarshalJSON is the SHIP serialization unmarshaller 64 | func (m *DeviceDiagnosisStateDataType) UnmarshalJSON(data []byte) error { 65 | return util.Unmarshal(data, &m) 66 | } 67 | 68 | // DeviceDiagnosisHeartbeatDataType complex type 69 | type DeviceDiagnosisHeartbeatDataType struct { 70 | Timestamp *string `json:"timestamp,omitempty"` 71 | HeartbeatCounter *uint64 `json:"heartbeatCounter,omitempty"` 72 | HeartbeatTimeout *string `json:"heartbeatTimeout,omitempty"` 73 | } 74 | 75 | // MarshalJSON is the SHIP serialization marshaller 76 | func (m DeviceDiagnosisHeartbeatDataType) MarshalJSON() ([]byte, error) { 77 | return util.Marshal(m) 78 | } 79 | 80 | // UnmarshalJSON is the SHIP serialization unmarshaller 81 | func (m *DeviceDiagnosisHeartbeatDataType) UnmarshalJSON(data []byte) error { 82 | return util.Unmarshal(data, &m) 83 | } 84 | 85 | // DeviceDiagnosisServiceDataType complex type 86 | type DeviceDiagnosisServiceDataType struct { 87 | Timestamp string `json:"timestamp"` 88 | InstallationTime string `json:"installationTime"` 89 | BootCounter *uint64 `json:"bootCounter"` 90 | NextService string `json:"nextService"` 91 | } 92 | 93 | // MarshalJSON is the SHIP serialization marshaller 94 | func (m DeviceDiagnosisServiceDataType) MarshalJSON() ([]byte, error) { 95 | return util.Marshal(m) 96 | } 97 | 98 | // UnmarshalJSON is the SHIP serialization unmarshaller 99 | func (m *DeviceDiagnosisServiceDataType) UnmarshalJSON(data []byte) error { 100 | return util.Unmarshal(data, &m) 101 | } 102 | -------------------------------------------------------------------------------- /spine/model/devicediagnosis_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestDeviceDiagnosisHeartbeatData(t *testing.T) { 10 | var specificationVersion SpecificationVersionType = "1.2.0" 11 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 12 | var deviceEvse AddressDeviceType = "d:_i:3210_EVSE" 13 | var addressSourceFeature AddressFeatureType = 5 14 | var addressSoureEntity1 AddressEntityType = 1 15 | var addressDestinationFeature AddressFeatureType = 5 16 | var addressDestinationEntity1 AddressEntityType = 1 17 | var msgCounter MsgCounterType = 5971 18 | var cmdClassifier CmdClassifierType = CmdClassifierTypeNotify 19 | 20 | var heartBeatCounter uint64 = 2245 21 | var heartBeatTimeout string = "PT4S" 22 | var timestamp string = "2006-05-04T18:13:51.0Z" 23 | 24 | datagram := CmiDatagramType{ 25 | Datagram: DatagramType{ 26 | Header: HeaderType{ 27 | SpecificationVersion: &specificationVersion, 28 | AddressSource: &FeatureAddressType{ 29 | Feature: &addressSourceFeature, 30 | Entity: []AddressEntityType{addressSoureEntity1}, 31 | Device: &deviceHems, 32 | }, 33 | AddressDestination: &FeatureAddressType{ 34 | Feature: &addressDestinationFeature, 35 | Entity: []AddressEntityType{addressDestinationEntity1}, 36 | Device: &deviceEvse, 37 | }, 38 | MsgCounter: &msgCounter, 39 | CmdClassifier: &cmdClassifier, 40 | }, 41 | Payload: PayloadType{ 42 | Cmd: []CmdType{{ 43 | DeviceDiagnosisHeartbeatData: &DeviceDiagnosisHeartbeatDataType{ 44 | Timestamp: ×tamp, 45 | HeartbeatCounter: &heartBeatCounter, 46 | HeartbeatTimeout: &heartBeatTimeout, 47 | }, 48 | }}, 49 | }, 50 | }, 51 | } 52 | datagramJSON, err := json.Marshal(datagram) 53 | if err != nil { 54 | t.Errorf("TestDeviceDiagnosisHeartbeatData() error = %v", err) 55 | } 56 | 57 | expectedJSON := `{"datagram":[{"header":[{"specificationVersion":"1.2.0"},{"addressSource":[{"device":"d:_i:3210_HEMS"},{"entity":[1]},{"feature":5}]},{"addressDestination":[{"device":"d:_i:3210_EVSE"},{"entity":[1]},{"feature":5}]},{"msgCounter":5971},{"cmdClassifier":"notify"}]},{"payload":[{"cmd":[[{"deviceDiagnosisHeartbeatData":[{"timestamp":"2006-05-04T18:13:51.0Z"},{"heartbeatCounter":2245},{"heartbeatTimeout":"PT4S"}]}]]}]}]}` 58 | expectedDatagram := CmiDatagramType{} 59 | err = json.Unmarshal(json.RawMessage(expectedJSON), &expectedDatagram) 60 | if err != nil { 61 | t.Errorf("TestDeviceDiagnosisHeartbeatData() Unmarshal failed error = %v", err) 62 | } else { 63 | if string(datagramJSON) != expectedJSON { 64 | fmt.Println("EXPECTED:") 65 | fmt.Println(string(expectedJSON)) 66 | fmt.Println("\nACTUAL:") 67 | fmt.Println(string(datagramJSON)) 68 | 69 | t.Errorf("TestDeviceDiagnosisHeartbeatData() actual json string doesn't match expected result") 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /spine/model/identification.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // IdentificationIdType type 9 | type IdentificationIdType uint 10 | 11 | // IdentificationTypeType type 12 | type IdentificationTypeType IdentificationTypeEnumType 13 | 14 | // IdentificationTypeEnumType type 15 | type IdentificationTypeEnumType string 16 | 17 | // IdentificationTypeEnumType constants 18 | const ( 19 | IdentificationTypeEnumTypeEui48 IdentificationTypeEnumType = "eui48" 20 | IdentificationTypeEnumTypeEui64 IdentificationTypeEnumType = "eui64" 21 | IdentificationTypeEnumTypeUserrfidtag IdentificationTypeEnumType = "userRfidTag" 22 | ) 23 | 24 | // IdentificationValueType type 25 | type IdentificationValueType string 26 | 27 | // IdentificationDataType complex type 28 | type IdentificationDataType struct { 29 | IdentificationId *IdentificationIdType `json:"identificationId,omitempty"` 30 | IdentificationType *IdentificationTypeType `json:"identificationType,omitempty"` 31 | IdentificationValue *IdentificationValueType `json:"identificationValue,omitempty"` 32 | Authorized *bool `json:"authorized,omitempty"` 33 | } 34 | 35 | // MarshalJSON is the SHIP serialization marshaller 36 | func (m IdentificationDataType) MarshalJSON() ([]byte, error) { 37 | return util.Marshal(m) 38 | } 39 | 40 | // UnmarshalJSON is the SHIP serialization unmarshaller 41 | func (m *IdentificationDataType) UnmarshalJSON(data []byte) error { 42 | return util.Unmarshal(data, &m) 43 | } 44 | 45 | // IdentificationListDataType complex type 46 | type IdentificationListDataType struct { 47 | IdentificationData []IdentificationDataType `json:"identificationData,omitempty"` 48 | } 49 | 50 | // MarshalJSON is the SHIP serialization marshaller 51 | func (m IdentificationListDataType) MarshalJSON() ([]byte, error) { 52 | return util.Marshal(m) 53 | } 54 | 55 | // UnmarshalJSON is the SHIP serialization unmarshaller 56 | func (m *IdentificationListDataType) UnmarshalJSON(data []byte) error { 57 | return util.Unmarshal(data, &m) 58 | } 59 | 60 | // IdentificationListDataSelectorsType complex type 61 | type IdentificationListDataSelectorsType struct { 62 | IdentificationId *IdentificationIdType `json:"identificationId,omitempty"` 63 | IdentificationType *IdentificationTypeType `json:"identificationType,omitempty"` 64 | } 65 | 66 | // MarshalJSON is the SHIP serialization marshaller 67 | func (m IdentificationListDataSelectorsType) MarshalJSON() ([]byte, error) { 68 | return util.Marshal(m) 69 | } 70 | 71 | // UnmarshalJSON is the SHIP serialization unmarshaller 72 | func (m *IdentificationListDataSelectorsType) UnmarshalJSON(data []byte) error { 73 | return util.Unmarshal(data, &m) 74 | } 75 | -------------------------------------------------------------------------------- /spine/model/identification_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestIdentificationListDataResponse(t *testing.T) { 10 | var specificationVersion SpecificationVersionType = "1.2.0" 11 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 12 | var deviceEvse AddressDeviceType = "d:_i:3210_EVSE" 13 | var addressSourceFeature AddressFeatureType = 10 14 | var addressSoureEntity1 AddressEntityType = 1 15 | var addressDestinationFeature AddressFeatureType = 8 16 | var addressDestinationEntity1 AddressEntityType = 1 17 | var msgCounter MsgCounterType = 247 18 | var msgCounterReference MsgCounterType = 4949 19 | var cmdClassifier CmdClassifierType = CmdClassifierTypeReply 20 | 21 | var identificationId IdentificationIdType = 0 22 | var identificationType IdentificationTypeType = IdentificationTypeType(IdentificationTypeEnumTypeEui48) 23 | var identificationValue IdentificationValueType = "" 24 | 25 | datagram := CmiDatagramType{ 26 | Datagram: DatagramType{ 27 | Header: HeaderType{ 28 | SpecificationVersion: &specificationVersion, 29 | AddressSource: &FeatureAddressType{ 30 | Feature: &addressSourceFeature, 31 | Entity: []AddressEntityType{addressSoureEntity1, addressSoureEntity1}, 32 | Device: &deviceEvse, 33 | }, 34 | AddressDestination: &FeatureAddressType{ 35 | Feature: &addressDestinationFeature, 36 | Entity: []AddressEntityType{addressDestinationEntity1}, 37 | Device: &deviceHems, 38 | }, 39 | MsgCounter: &msgCounter, 40 | MsgCounterReference: &msgCounterReference, 41 | CmdClassifier: &cmdClassifier, 42 | }, 43 | Payload: PayloadType{ 44 | Cmd: []CmdType{{ 45 | IdentificationListData: &IdentificationListDataType{ 46 | IdentificationData: []IdentificationDataType{ 47 | { 48 | IdentificationId: &identificationId, 49 | IdentificationType: &identificationType, 50 | IdentificationValue: &identificationValue, 51 | }, 52 | }, 53 | }, 54 | }}, 55 | }, 56 | }, 57 | } 58 | json, err := json.Marshal(datagram) 59 | if err != nil { 60 | t.Errorf("TestIdentificationListDataResponse() error = %v", err) 61 | } 62 | jsonString := string(json) 63 | 64 | jsonTest := `{"datagram":[{"header":[{"specificationVersion":"1.2.0"},{"addressSource":[{"device":"d:_i:3210_EVSE"},{"entity":[1,1]},{"feature":10}]},{"addressDestination":[{"device":"d:_i:3210_HEMS"},{"entity":[1]},{"feature":8}]},{"msgCounter":247},{"msgCounterReference":4949},{"cmdClassifier":"reply"}]},{"payload":[{"cmd":[[{"identificationListData":[{"identificationData":[[{"identificationId":0},{"identificationType":"eui48"},{"identificationValue":""}]]}]}]]}]}]}` 65 | if jsonString != jsonTest { 66 | fmt.Println("EXPECTED:") 67 | fmt.Println(string(jsonTest)) 68 | fmt.Println("\nACTUAL:") 69 | fmt.Println(string(jsonString)) 70 | 71 | t.Errorf("TestIdentificationListDataResponse() actual json string doesn't match expected result") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /spine/model/incentivetable.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // IncentiveTableType complex type 9 | type IncentiveTableType struct { 10 | Tariff *TariffDataType `json:"tariff,omitempty"` 11 | IncentiveSlot []IncentiveTableIncentiveSlotType `json:"incentiveSlot,omitempty"` 12 | } 13 | 14 | // MarshalJSON is the SHIP serialization marshaller 15 | func (m IncentiveTableType) MarshalJSON() ([]byte, error) { 16 | return util.Marshal(m) 17 | } 18 | 19 | // UnmarshalJSON is the SHIP serialization unmarshaller 20 | func (m *IncentiveTableType) UnmarshalJSON(data []byte) error { 21 | return util.Unmarshal(data, &m) 22 | } 23 | 24 | // IncentiveTableIncentiveSlotType complex type 25 | type IncentiveTableIncentiveSlotType struct { 26 | TimeInterval *TimeTableDataType `json:"timeInterval,omitempty"` 27 | Tier []IncentiveTableTierType `json:"tier,omitempty"` 28 | } 29 | 30 | // MarshalJSON is the SHIP serialization marshaller 31 | func (m IncentiveTableIncentiveSlotType) MarshalJSON() ([]byte, error) { 32 | return util.Marshal(m) 33 | } 34 | 35 | // UnmarshalJSON is the SHIP serialization unmarshaller 36 | func (m *IncentiveTableIncentiveSlotType) UnmarshalJSON(data []byte) error { 37 | return util.Unmarshal(data, &m) 38 | } 39 | 40 | // IncentiveTableTierType complex type 41 | type IncentiveTableTierType struct { 42 | Tier *TierDataType `json:"tier,omitempty"` 43 | Boundary []TierBoundaryDataType `json:"boundary,omitempty"` 44 | Incentive []IncentiveDataType `json:"incentive,omitempty"` 45 | } 46 | 47 | // MarshalJSON is the SHIP serialization marshaller 48 | func (m IncentiveTableTierType) MarshalJSON() ([]byte, error) { 49 | return util.Marshal(m) 50 | } 51 | 52 | // UnmarshalJSON is the SHIP serialization unmarshaller 53 | func (m *IncentiveTableTierType) UnmarshalJSON(data []byte) error { 54 | return util.Unmarshal(data, &m) 55 | } 56 | 57 | // IncentiveTableDataType complex type 58 | type IncentiveTableDataType struct { 59 | IncentiveTable []IncentiveTableType `json:"incentiveTable,omitempty"` 60 | } 61 | 62 | // MarshalJSON is the SHIP serialization marshaller 63 | func (m IncentiveTableDataType) MarshalJSON() ([]byte, error) { 64 | return util.Marshal(m) 65 | } 66 | 67 | // UnmarshalJSON is the SHIP serialization unmarshaller 68 | func (m *IncentiveTableDataType) UnmarshalJSON(data []byte) error { 69 | return util.Unmarshal(data, &m) 70 | } 71 | 72 | // IncentiveTableDataSelectorsType complex type 73 | type IncentiveTableDataSelectorsType struct { 74 | Tariff *TariffListDataSelectorsType `json:"tariff,omitempty"` 75 | } 76 | 77 | // MarshalJSON is the SHIP serialization marshaller 78 | func (m IncentiveTableDataSelectorsType) MarshalJSON() ([]byte, error) { 79 | return util.Marshal(m) 80 | } 81 | 82 | // UnmarshalJSON is the SHIP serialization unmarshaller 83 | func (m *IncentiveTableDataSelectorsType) UnmarshalJSON(data []byte) error { 84 | return util.Unmarshal(data, &m) 85 | } 86 | 87 | // IncentiveTableDescriptionType complex type 88 | type IncentiveTableDescriptionType struct { 89 | TariffDescription *TariffDescriptionDataType `json:"tariffDescription,omitempty"` 90 | Tier []IncentiveTableDescriptionTierType `json:"tier,omitempty"` 91 | } 92 | 93 | // MarshalJSON is the SHIP serialization marshaller 94 | func (m IncentiveTableDescriptionType) MarshalJSON() ([]byte, error) { 95 | return util.Marshal(m) 96 | } 97 | 98 | // UnmarshalJSON is the SHIP serialization unmarshaller 99 | func (m *IncentiveTableDescriptionType) UnmarshalJSON(data []byte) error { 100 | return util.Unmarshal(data, &m) 101 | } 102 | 103 | // IncentiveTableDescriptionTierType complex type 104 | type IncentiveTableDescriptionTierType struct { 105 | TierDescription *TierDescriptionDataType `json:"tierDescription,omitempty"` 106 | BoundaryDescription []TierBoundaryDescriptionDataType `json:"boundaryDescription,omitempty"` 107 | IncentiveDescription []IncentiveDescriptionDataType `json:"incentiveDescription,omitempty"` 108 | } 109 | 110 | // MarshalJSON is the SHIP serialization marshaller 111 | func (m IncentiveTableDescriptionTierType) MarshalJSON() ([]byte, error) { 112 | return util.Marshal(m) 113 | } 114 | 115 | // UnmarshalJSON is the SHIP serialization unmarshaller 116 | func (m *IncentiveTableDescriptionTierType) UnmarshalJSON(data []byte) error { 117 | return util.Unmarshal(data, &m) 118 | } 119 | 120 | // IncentiveTableDescriptionDataType complex type 121 | type IncentiveTableDescriptionDataType struct { 122 | IncentiveTableDescription []IncentiveTableDescriptionType `json:"incentiveTableDescription,omitempty"` 123 | } 124 | 125 | // MarshalJSON is the SHIP serialization marshaller 126 | func (m IncentiveTableDescriptionDataType) MarshalJSON() ([]byte, error) { 127 | return util.Marshal(m) 128 | } 129 | 130 | // UnmarshalJSON is the SHIP serialization unmarshaller 131 | func (m *IncentiveTableDescriptionDataType) UnmarshalJSON(data []byte) error { 132 | return util.Unmarshal(data, &m) 133 | } 134 | 135 | // IncentiveTableDescriptionDataSelectorsType complex type 136 | type IncentiveTableDescriptionDataSelectorsType struct { 137 | TariffDescription *TariffDescriptionListDataSelectorsType `json:"tariffDescription,omitempty"` 138 | } 139 | 140 | // MarshalJSON is the SHIP serialization marshaller 141 | func (m IncentiveTableDescriptionDataSelectorsType) MarshalJSON() ([]byte, error) { 142 | return util.Marshal(m) 143 | } 144 | 145 | // UnmarshalJSON is the SHIP serialization unmarshaller 146 | func (m *IncentiveTableDescriptionDataSelectorsType) UnmarshalJSON(data []byte) error { 147 | return util.Unmarshal(data, &m) 148 | } 149 | 150 | // IncentiveTableConstraintsType complex type 151 | type IncentiveTableConstraintsType struct { 152 | Tariff *TariffDataType `json:"tariff,omitempty"` 153 | TariffConstraints *TariffOverallConstraintsDataType `json:"tariffConstraints,omitempty"` 154 | IncentiveSlotConstraints *TimeTableConstraintsDataType `json:"incentiveSlotConstraints,omitempty"` 155 | } 156 | 157 | // MarshalJSON is the SHIP serialization marshaller 158 | func (m IncentiveTableConstraintsType) MarshalJSON() ([]byte, error) { 159 | return util.Marshal(m) 160 | } 161 | 162 | // UnmarshalJSON is the SHIP serialization unmarshaller 163 | func (m *IncentiveTableConstraintsType) UnmarshalJSON(data []byte) error { 164 | return util.Unmarshal(data, &m) 165 | } 166 | 167 | // IncentiveTableConstraintsDataType complex type 168 | type IncentiveTableConstraintsDataType struct { 169 | IncentiveTableConstraints []IncentiveTableConstraintsType `json:"incentiveTableConstraints,omitempty"` 170 | } 171 | 172 | // MarshalJSON is the SHIP serialization marshaller 173 | func (m IncentiveTableConstraintsDataType) MarshalJSON() ([]byte, error) { 174 | return util.Marshal(m) 175 | } 176 | 177 | // UnmarshalJSON is the SHIP serialization unmarshaller 178 | func (m *IncentiveTableConstraintsDataType) UnmarshalJSON(data []byte) error { 179 | return util.Unmarshal(data, &m) 180 | } 181 | 182 | // IncentiveTableConstraintsDataSelectorsType complex type 183 | type IncentiveTableConstraintsDataSelectorsType struct { 184 | Tariff *TariffListDataSelectorsType `json:"tariff,omitempty"` 185 | } 186 | 187 | // MarshalJSON is the SHIP serialization marshaller 188 | func (m IncentiveTableConstraintsDataSelectorsType) MarshalJSON() ([]byte, error) { 189 | return util.Marshal(m) 190 | } 191 | 192 | // UnmarshalJSON is the SHIP serialization unmarshaller 193 | func (m *IncentiveTableConstraintsDataSelectorsType) UnmarshalJSON(data []byte) error { 194 | return util.Unmarshal(data, &m) 195 | } 196 | -------------------------------------------------------------------------------- /spine/model/loadcontrol_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestLoadControlLimitListDataWrite(t *testing.T) { 10 | var specificationVersion SpecificationVersionType = "1.2.0" 11 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 12 | var deviceEvse AddressDeviceType = "d:_i:3210_EVSE" 13 | var addressSourceFeature AddressFeatureType = 7 14 | var addressSoureEntity1 AddressEntityType = 1 15 | var addressDestinationFeature AddressFeatureType = 1 16 | var addressDestinationEntity1 AddressEntityType = 1 17 | var msgCounter MsgCounterType = 5014 18 | var cmdClassifier CmdClassifierType = CmdClassifierTypeWrite 19 | var ackRequest bool = true 20 | 21 | var limitId1 LoadControlLimitIdType = 1 22 | var isLimitActive1 bool = true 23 | var valueNumber1 NumberType = 0 24 | var valueScale1 ScaleType = 0 25 | var limitId2 LoadControlLimitIdType = 2 26 | var isLimitActive2 bool = true 27 | var valueNumber2 NumberType = 0 28 | var valueScale2 ScaleType = 0 29 | 30 | datagram := CmiDatagramType{ 31 | Datagram: DatagramType{ 32 | Header: HeaderType{ 33 | SpecificationVersion: &specificationVersion, 34 | AddressSource: &FeatureAddressType{ 35 | Feature: &addressSourceFeature, 36 | Entity: []AddressEntityType{addressSoureEntity1}, 37 | Device: &deviceHems, 38 | }, 39 | AddressDestination: &FeatureAddressType{ 40 | Feature: &addressDestinationFeature, 41 | Entity: []AddressEntityType{addressDestinationEntity1, addressDestinationEntity1}, 42 | Device: &deviceEvse, 43 | }, 44 | MsgCounter: &msgCounter, 45 | CmdClassifier: &cmdClassifier, 46 | AckRequest: &ackRequest, 47 | }, 48 | Payload: PayloadType{ 49 | Cmd: []CmdType{{ 50 | LoadControlLimitListData: &LoadControlLimitListDataType{ 51 | LoadControlLimitData: []LoadControlLimitDataType{ 52 | { 53 | LimitId: &limitId1, 54 | IsLimitActive: &isLimitActive1, 55 | Value: &ScaledNumberType{ 56 | Number: &valueNumber1, 57 | Scale: &valueScale1, 58 | }, 59 | }, 60 | { 61 | LimitId: &limitId2, 62 | IsLimitActive: &isLimitActive2, 63 | Value: &ScaledNumberType{ 64 | Number: &valueNumber2, 65 | Scale: &valueScale2, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }}, 71 | }, 72 | }, 73 | } 74 | json, err := json.Marshal(datagram) 75 | if err != nil { 76 | t.Errorf("TestLoadControlLimitListDataWrite() error = %v", err) 77 | } 78 | jsonString := string(json) 79 | 80 | jsonTest := `{"datagram":[{"header":[{"specificationVersion":"1.2.0"},{"addressSource":[{"device":"d:_i:3210_HEMS"},{"entity":[1]},{"feature":7}]},{"addressDestination":[{"device":"d:_i:3210_EVSE"},{"entity":[1,1]},{"feature":1}]},{"msgCounter":5014},{"cmdClassifier":"write"},{"ackRequest":true}]},{"payload":[{"cmd":[[{"loadControlLimitListData":[{"loadControlLimitData":[[{"limitId":1},{"isLimitActive":true},{"value":[{"number":0},{"scale":0}]}],[{"limitId":2},{"isLimitActive":true},{"value":[{"number":0},{"scale":0}]}]]}]}]]}]}]}` 81 | if jsonString != jsonTest { 82 | fmt.Println("EXPECTED:") 83 | fmt.Println(string(jsonTest)) 84 | fmt.Println("\nACTUAL:") 85 | fmt.Println(string(jsonString)) 86 | 87 | t.Errorf("TestLoadControlLimitListDataWrite() actual json string doesn't match expected result") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /spine/model/result.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // ErrorNumberType type 9 | type ErrorNumberType uint 10 | 11 | // ResultDataType complex type 12 | type ResultDataType struct { 13 | ErrorNumber *ErrorNumberType `json:"errorNumber,omitempty"` 14 | Description *DescriptionType `json:"description,omitempty"` 15 | } 16 | 17 | // MarshalJSON is the SHIP serialization marshaller 18 | func (m ResultDataType) MarshalJSON() ([]byte, error) { 19 | return util.Marshal(m) 20 | } 21 | 22 | // UnmarshalJSON is the SHIP serialization unmarshaller 23 | func (m *ResultDataType) UnmarshalJSON(data []byte) error { 24 | return util.Unmarshal(data, &m) 25 | } 26 | -------------------------------------------------------------------------------- /spine/model/subscriptionmanagement.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // SubscriptionIdType type 9 | type SubscriptionIdType uint 10 | 11 | // SubscriptionManagementEntryDataType complex type 12 | type SubscriptionManagementEntryDataType struct { 13 | SubscriptionId *SubscriptionIdType `json:"subscriptionId,omitempty"` 14 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 15 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 16 | Label *LabelType `json:"label,omitempty"` 17 | Description *DescriptionType `json:"description,omitempty"` 18 | } 19 | 20 | // MarshalJSON is the SHIP serialization marshaller 21 | func (m SubscriptionManagementEntryDataType) MarshalJSON() ([]byte, error) { 22 | return util.Marshal(m) 23 | } 24 | 25 | // UnmarshalJSON is the SHIP serialization unmarshaller 26 | func (m *SubscriptionManagementEntryDataType) UnmarshalJSON(data []byte) error { 27 | return util.Unmarshal(data, &m) 28 | } 29 | 30 | // SubscriptionManagementEntryListDataType complex type 31 | type SubscriptionManagementEntryListDataType struct { 32 | SubscriptionManagementEntryData []SubscriptionManagementEntryDataType `json:"subscriptionManagementEntryData,omitempty"` 33 | } 34 | 35 | // MarshalJSON is the SHIP serialization marshaller 36 | func (m SubscriptionManagementEntryListDataType) MarshalJSON() ([]byte, error) { 37 | return util.Marshal(m) 38 | } 39 | 40 | // UnmarshalJSON is the SHIP serialization unmarshaller 41 | func (m *SubscriptionManagementEntryListDataType) UnmarshalJSON(data []byte) error { 42 | return util.Unmarshal(data, &m) 43 | } 44 | 45 | // SubscriptionManagementEntryListDataSelectorsType complex type 46 | type SubscriptionManagementEntryListDataSelectorsType struct { 47 | SubscriptionId *SubscriptionIdType `json:"subscriptionId,omitempty"` 48 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 49 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 50 | } 51 | 52 | // MarshalJSON is the SHIP serialization marshaller 53 | func (m SubscriptionManagementEntryListDataSelectorsType) MarshalJSON() ([]byte, error) { 54 | return util.Marshal(m) 55 | } 56 | 57 | // UnmarshalJSON is the SHIP serialization unmarshaller 58 | func (m *SubscriptionManagementEntryListDataSelectorsType) UnmarshalJSON(data []byte) error { 59 | return util.Unmarshal(data, &m) 60 | } 61 | 62 | // SubscriptionManagementRequestCallType complex type 63 | type SubscriptionManagementRequestCallType struct { 64 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 65 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 66 | ServerFeatureType *FeatureTypeType `json:"serverFeatureType,omitempty"` 67 | } 68 | 69 | // MarshalJSON is the SHIP serialization marshaller 70 | func (m SubscriptionManagementRequestCallType) MarshalJSON() ([]byte, error) { 71 | return util.Marshal(m) 72 | } 73 | 74 | // UnmarshalJSON is the SHIP serialization unmarshaller 75 | func (m *SubscriptionManagementRequestCallType) UnmarshalJSON(data []byte) error { 76 | return util.Unmarshal(data, &m) 77 | } 78 | 79 | // SubscriptionManagementDeleteCallType complex type 80 | type SubscriptionManagementDeleteCallType struct { 81 | SubscriptionId *SubscriptionIdType `json:"subscriptionId,omitempty"` 82 | ClientAddress *FeatureAddressType `json:"clientAddress,omitempty"` 83 | ServerAddress *FeatureAddressType `json:"serverAddress,omitempty"` 84 | } 85 | 86 | // MarshalJSON is the SHIP serialization marshaller 87 | func (m SubscriptionManagementDeleteCallType) MarshalJSON() ([]byte, error) { 88 | return util.Marshal(m) 89 | } 90 | 91 | // UnmarshalJSON is the SHIP serialization unmarshaller 92 | func (m *SubscriptionManagementDeleteCallType) UnmarshalJSON(data []byte) error { 93 | return util.Unmarshal(data, &m) 94 | } 95 | -------------------------------------------------------------------------------- /spine/model/timetable.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // TimeTableIdType type 9 | type TimeTableIdType uint 10 | 11 | // TimeSlotIdType type 12 | type TimeSlotIdType uint 13 | 14 | // TimeSlotCountType type 15 | type TimeSlotCountType TimeSlotIdType 16 | 17 | // TimeSlotTimeModeType type 18 | type TimeSlotTimeModeType TimeSlotTimeModeEnumType 19 | 20 | // TimeSlotTimeModeEnumType type 21 | type TimeSlotTimeModeEnumType string 22 | 23 | // TimeSlotTimeModeEnumType constants 24 | const ( 25 | TimeSlotTimeModeEnumTypeAbsolute TimeSlotTimeModeEnumType = "absolute" 26 | TimeSlotTimeModeEnumTypeRecurring TimeSlotTimeModeEnumType = "recurring" 27 | TimeSlotTimeModeEnumTypeBoth TimeSlotTimeModeEnumType = "both" 28 | ) 29 | 30 | // TimeTableDataType complex type 31 | type TimeTableDataType struct { 32 | TimeTableId *TimeTableIdType `json:"timeTableId,omitempty"` 33 | TimeSlotId *TimeSlotIdType `json:"timeSlotId,omitempty"` 34 | RecurrenceInformation *RecurrenceInformationType `json:"recurrenceInformation,omitempty"` 35 | StartTime *AbsoluteOrRecurringTimeType `json:"startTime,omitempty"` 36 | EndTime *AbsoluteOrRecurringTimeType `json:"endTime,omitempty"` 37 | } 38 | 39 | // MarshalJSON is the SHIP serialization marshaller 40 | func (m TimeTableDataType) MarshalJSON() ([]byte, error) { 41 | return util.Marshal(m) 42 | } 43 | 44 | // UnmarshalJSON is the SHIP serialization unmarshaller 45 | func (m *TimeTableDataType) UnmarshalJSON(data []byte) error { 46 | return util.Unmarshal(data, &m) 47 | } 48 | 49 | // TimeTableListDataType complex type 50 | type TimeTableListDataType struct { 51 | TimeTableData []TimeTableDataType `json:"timeTableData,omitempty"` 52 | } 53 | 54 | // MarshalJSON is the SHIP serialization marshaller 55 | func (m TimeTableListDataType) MarshalJSON() ([]byte, error) { 56 | return util.Marshal(m) 57 | } 58 | 59 | // UnmarshalJSON is the SHIP serialization unmarshaller 60 | func (m *TimeTableListDataType) UnmarshalJSON(data []byte) error { 61 | return util.Unmarshal(data, &m) 62 | } 63 | 64 | // TimeTableListDataSelectorsType complex type 65 | type TimeTableListDataSelectorsType struct { 66 | TimeTableId *TimeTableIdType `json:"timeTableId,omitempty"` 67 | TimeSlotId *TimeSlotIdType `json:"timeSlotId,omitempty"` 68 | } 69 | 70 | // MarshalJSON is the SHIP serialization marshaller 71 | func (m TimeTableListDataSelectorsType) MarshalJSON() ([]byte, error) { 72 | return util.Marshal(m) 73 | } 74 | 75 | // UnmarshalJSON is the SHIP serialization unmarshaller 76 | func (m *TimeTableListDataSelectorsType) UnmarshalJSON(data []byte) error { 77 | return util.Unmarshal(data, &m) 78 | } 79 | 80 | // TimeTableConstraintsDataType complex type 81 | type TimeTableConstraintsDataType struct { 82 | TimeTableId *uint `json:"timeTableId,omitempty"` 83 | SlotCountMin *TimeSlotCountType `json:"slotCountMin,omitempty"` 84 | SlotCountMax *TimeSlotCountType `json:"slotCountMax,omitempty"` 85 | SlotDurationMin *string `json:"slotDurationMin,omitempty"` 86 | SlotDurationMax *string `json:"slotDurationMax,omitempty"` 87 | SlotDurationStepSize *string `json:"slotDurationStepSize,omitempty"` 88 | SlotShiftStepSize *string `json:"slotShiftStepSize,omitempty"` 89 | FirstSlotBeginsAt *string `json:"firstSlotBeginsAt,omitempty"` 90 | } 91 | 92 | // MarshalJSON is the SHIP serialization marshaller 93 | func (m TimeTableConstraintsDataType) MarshalJSON() ([]byte, error) { 94 | return util.Marshal(m) 95 | } 96 | 97 | // UnmarshalJSON is the SHIP serialization unmarshaller 98 | func (m *TimeTableConstraintsDataType) UnmarshalJSON(data []byte) error { 99 | return util.Unmarshal(data, &m) 100 | } 101 | 102 | // TimeTableConstraintsListDataType complex type 103 | type TimeTableConstraintsListDataType struct { 104 | TimeTableConstraintsData []TimeTableConstraintsDataType `json:"timeTableConstraintsData,omitempty"` 105 | } 106 | 107 | // MarshalJSON is the SHIP serialization marshaller 108 | func (m TimeTableConstraintsListDataType) MarshalJSON() ([]byte, error) { 109 | return util.Marshal(m) 110 | } 111 | 112 | // UnmarshalJSON is the SHIP serialization unmarshaller 113 | func (m *TimeTableConstraintsListDataType) UnmarshalJSON(data []byte) error { 114 | return util.Unmarshal(data, &m) 115 | } 116 | 117 | // TimeTableConstraintsListDataSelectorsType complex type 118 | type TimeTableConstraintsListDataSelectorsType struct { 119 | TimeTableId *TimeTableIdType `json:"timeTableId,omitempty"` 120 | } 121 | 122 | // MarshalJSON is the SHIP serialization marshaller 123 | func (m TimeTableConstraintsListDataSelectorsType) MarshalJSON() ([]byte, error) { 124 | return util.Marshal(m) 125 | } 126 | 127 | // UnmarshalJSON is the SHIP serialization unmarshaller 128 | func (m *TimeTableConstraintsListDataSelectorsType) UnmarshalJSON(data []byte) error { 129 | return util.Unmarshal(data, &m) 130 | } 131 | 132 | // TimeTableDescriptionDataType complex type 133 | type TimeTableDescriptionDataType struct { 134 | TimeTableId *uint `json:"timeTableId,omitempty"` 135 | TimeSlotCountChangeable *bool `json:"timeSlotCountChangeable,omitempty"` 136 | TimeSlotTimesChangeable *bool `json:"timeSlotTimesChangeable,omitempty"` 137 | TimeSlotTimeMode *TimeSlotTimeModeType `json:"timeSlotTimeMode,omitempty"` 138 | Label *LabelType `json:"label,omitempty"` 139 | Description *DescriptionType `json:"description,omitempty"` 140 | } 141 | 142 | // MarshalJSON is the SHIP serialization marshaller 143 | func (m TimeTableDescriptionDataType) MarshalJSON() ([]byte, error) { 144 | return util.Marshal(m) 145 | } 146 | 147 | // UnmarshalJSON is the SHIP serialization unmarshaller 148 | func (m *TimeTableDescriptionDataType) UnmarshalJSON(data []byte) error { 149 | return util.Unmarshal(data, &m) 150 | } 151 | 152 | // TimeTableDescriptionListDataType complex type 153 | type TimeTableDescriptionListDataType struct { 154 | TimeTableDescriptionData []TimeTableDescriptionDataType `json:"timeTableDescriptionData,omitempty"` 155 | } 156 | 157 | // MarshalJSON is the SHIP serialization marshaller 158 | func (m TimeTableDescriptionListDataType) MarshalJSON() ([]byte, error) { 159 | return util.Marshal(m) 160 | } 161 | 162 | // UnmarshalJSON is the SHIP serialization unmarshaller 163 | func (m *TimeTableDescriptionListDataType) UnmarshalJSON(data []byte) error { 164 | return util.Unmarshal(data, &m) 165 | } 166 | 167 | // TimeTableDescriptionListDataSelectorsType complex type 168 | type TimeTableDescriptionListDataSelectorsType struct { 169 | TimeTableId *TimeTableIdType `json:"timeTableId,omitempty"` 170 | } 171 | 172 | // MarshalJSON is the SHIP serialization marshaller 173 | func (m TimeTableDescriptionListDataSelectorsType) MarshalJSON() ([]byte, error) { 174 | return util.Marshal(m) 175 | } 176 | 177 | // UnmarshalJSON is the SHIP serialization unmarshaller 178 | func (m *TimeTableDescriptionListDataSelectorsType) UnmarshalJSON(data []byte) error { 179 | return util.Unmarshal(data, &m) 180 | } 181 | -------------------------------------------------------------------------------- /spine/model/usecaseinformation.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // UseCaseActorType type 9 | type UseCaseActorType UseCaseActorEnumType 10 | 11 | // UseCaseActorEnumType type 12 | type UseCaseActorEnumType string 13 | 14 | const ( 15 | UseCaseActorEnumTypeEV UseCaseActorEnumType = "EV" 16 | ) 17 | 18 | // UseCaseNameType type 19 | type UseCaseNameType UseCaseNameEnumType 20 | 21 | // UseCaseNameEnumType type 22 | type UseCaseNameEnumType string 23 | 24 | const ( 25 | UseCaseNameEnumTypeMeasurementOfElectricityDuringEVCharging UseCaseNameEnumType = "measurementOfElectricityDuringEvCharging" 26 | UseCaseNameEnumTypeOptimizationOfSelfConsumptionDuringEVCharging UseCaseNameEnumType = "optimizationOfSelfConsumptionDuringEvCharging" 27 | UseCaseNameEnumTypeOverloadProtectionByEVChargingCurrentCurtailment UseCaseNameEnumType = "overloadProtectionByEvChargingCurrentCurtailment" 28 | UseCaseNameEnumTypeCoordinatedEVCharging UseCaseNameEnumType = "coordinatedEvCharging" 29 | UseCaseNameEnumTypeEVCommissioningAndConfiguration UseCaseNameEnumType = "evCommissioningAndConfiguration" 30 | UseCaseNameEnumTypeEVSECommissioningAndConfiguration UseCaseNameEnumType = "evseCommissioningAndConfiguration" 31 | UseCaseNameEnumTypeEVChargingSummary UseCaseNameEnumType = "evChargingSummary" 32 | UseCaseNameEnumTypeEVStateOfCharge UseCaseNameEnumType = "evStateOfCharge" 33 | ) 34 | 35 | // UseCaseScenarioSupportType type 36 | type UseCaseScenarioSupportType uint 37 | 38 | // UseCaseSupportType complex type 39 | type UseCaseSupportType struct { 40 | UseCaseName *UseCaseNameType `json:"useCaseName,omitempty"` 41 | UseCaseVersion *SpecificationVersionType `json:"useCaseVersion,omitempty"` 42 | UseCaseAvailable *bool `json:"useCaseAvailable,omitempty"` 43 | ScenarioSupport []UseCaseScenarioSupportType `json:"scenarioSupport,omitempty"` 44 | } 45 | 46 | // MarshalJSON is the SHIP serialization marshaller 47 | func (m UseCaseSupportType) MarshalJSON() ([]byte, error) { 48 | return util.Marshal(m) 49 | } 50 | 51 | // UnmarshalJSON is the SHIP serialization unmarshaller 52 | func (m *UseCaseSupportType) UnmarshalJSON(data []byte) error { 53 | return util.Unmarshal(data, &m) 54 | } 55 | 56 | // UseCaseSupportSelectorsType complex type 57 | type UseCaseSupportSelectorsType struct { 58 | UseCaseName *UseCaseNameType `json:"useCaseName,omitempty"` 59 | UseCaseVersion *SpecificationVersionType `json:"useCaseVersion,omitempty"` 60 | ScenarioSupport *UseCaseScenarioSupportType `json:"scenarioSupport,omitempty"` 61 | } 62 | 63 | // MarshalJSON is the SHIP serialization marshaller 64 | func (m UseCaseSupportSelectorsType) MarshalJSON() ([]byte, error) { 65 | return util.Marshal(m) 66 | } 67 | 68 | // UnmarshalJSON is the SHIP serialization unmarshaller 69 | func (m *UseCaseSupportSelectorsType) UnmarshalJSON(data []byte) error { 70 | return util.Unmarshal(data, &m) 71 | } 72 | 73 | // UseCaseInformationDataType complex type 74 | type UseCaseInformationDataType struct { 75 | Address *FeatureAddressType `json:"address,omitempty"` 76 | Actor *UseCaseActorType `json:"actor,omitempty"` 77 | UseCaseSupport []UseCaseSupportType `json:"useCaseSupport,omitempty"` 78 | } 79 | 80 | // MarshalJSON is the SHIP serialization marshaller 81 | func (m UseCaseInformationDataType) MarshalJSON() ([]byte, error) { 82 | return util.Marshal(m) 83 | } 84 | 85 | // UnmarshalJSON is the SHIP serialization unmarshaller 86 | func (m *UseCaseInformationDataType) UnmarshalJSON(data []byte) error { 87 | return util.Unmarshal(data, &m) 88 | } 89 | 90 | // UseCaseInformationListDataType complex type 91 | type UseCaseInformationListDataType struct { 92 | UseCaseInformationData []UseCaseInformationDataType `json:"useCaseInformationData,omitempty"` 93 | } 94 | 95 | // MarshalJSON is the SHIP serialization marshaller 96 | func (m UseCaseInformationListDataType) MarshalJSON() ([]byte, error) { 97 | return util.Marshal(m) 98 | } 99 | 100 | // UnmarshalJSON is the SHIP serialization unmarshaller 101 | func (m *UseCaseInformationListDataType) UnmarshalJSON(data []byte) error { 102 | return util.Unmarshal(data, &m) 103 | } 104 | 105 | // UseCaseInformationListDataSelectorsType complex type 106 | type UseCaseInformationListDataSelectorsType struct { 107 | Address *FeatureAddressType `json:"address,omitempty"` 108 | Actor *UseCaseActorType `json:"actor,omitempty"` 109 | UseCaseSupport *UseCaseSupportSelectorsType `json:"useCaseSupport,omitempty"` 110 | } 111 | 112 | // MarshalJSON is the SHIP serialization marshaller 113 | func (m UseCaseInformationListDataSelectorsType) MarshalJSON() ([]byte, error) { 114 | return util.Marshal(m) 115 | } 116 | 117 | // UnmarshalJSON is the SHIP serialization unmarshaller 118 | func (m *UseCaseInformationListDataSelectorsType) UnmarshalJSON(data []byte) error { 119 | return util.Unmarshal(data, &m) 120 | } 121 | -------------------------------------------------------------------------------- /spine/model/usecaseinformation_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestUseCaseInformationReply(t *testing.T) { 10 | var deviceHems AddressDeviceType = "d:_i:3210_HEMS" 11 | var actor UseCaseActorType = "CEM" 12 | var useCaseVersion SpecificationVersionType = "1.0.1" 13 | var useCaseName1 UseCaseNameType = "evseCommissioningAndConfiguration" 14 | var useCaseName2 UseCaseNameType = "evChargingSummary" 15 | var useCaseName3 UseCaseNameType = "measurementOfElectricityDuringEvCharging" 16 | var useCaseName4 UseCaseNameType = "optimizationOfSelfConsumptionDuringEvCharging" 17 | var useCaseName5 UseCaseNameType = "coordinatedEvCharging" 18 | var useCaseName6 UseCaseNameType = "overloadProtectionByEvChargingCurrentCurtailment" 19 | var useCaseName7 UseCaseNameType = "evCommissioningAndConfiguration" 20 | 21 | payload := PayloadType{ 22 | Cmd: []CmdType{{ 23 | NodeManagementUseCaseData: &NodeManagementUseCaseDataType{ 24 | UseCaseInformation: []UseCaseInformationDataType{ 25 | { 26 | Address: &FeatureAddressType{Device: &deviceHems}, 27 | Actor: &actor, 28 | UseCaseSupport: []UseCaseSupportType{ 29 | { 30 | UseCaseName: &useCaseName1, 31 | UseCaseVersion: &useCaseVersion, 32 | ScenarioSupport: []UseCaseScenarioSupportType{1, 2}, 33 | }, 34 | { 35 | UseCaseName: &useCaseName2, 36 | UseCaseVersion: &useCaseVersion, 37 | ScenarioSupport: []UseCaseScenarioSupportType{1}, 38 | }, 39 | { 40 | UseCaseName: &useCaseName3, 41 | UseCaseVersion: &useCaseVersion, 42 | ScenarioSupport: []UseCaseScenarioSupportType{1, 2, 3}, 43 | }, 44 | { 45 | UseCaseName: &useCaseName4, 46 | UseCaseVersion: &useCaseVersion, 47 | ScenarioSupport: []UseCaseScenarioSupportType{1, 2, 3}, 48 | }, 49 | { 50 | UseCaseName: &useCaseName5, 51 | UseCaseVersion: &useCaseVersion, 52 | ScenarioSupport: []UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, 53 | }, 54 | { 55 | UseCaseName: &useCaseName6, 56 | UseCaseVersion: &useCaseVersion, 57 | ScenarioSupport: []UseCaseScenarioSupportType{1, 2, 3}, 58 | }, 59 | { 60 | UseCaseName: &useCaseName7, 61 | UseCaseVersion: &useCaseVersion, 62 | ScenarioSupport: []UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }}, 69 | } 70 | 71 | json, err := json.Marshal(payload) 72 | if err != nil { 73 | t.Errorf("TestUseCaseInformationReply() error = %v", err) 74 | } 75 | jsonString := string(json) 76 | 77 | jsonTest := `[{"cmd":[[{"nodeManagementUseCaseData":[{"useCaseInformation":[[{"address":[{"device":"d:_i:3210_HEMS"}]},{"actor":"CEM"},{"useCaseSupport":[[{"useCaseName":"evseCommissioningAndConfiguration"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1,2]}],[{"useCaseName":"evChargingSummary"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1]}],[{"useCaseName":"measurementOfElectricityDuringEvCharging"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1,2,3]}],[{"useCaseName":"optimizationOfSelfConsumptionDuringEvCharging"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1,2,3]}],[{"useCaseName":"coordinatedEvCharging"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1,2,3,4,5,6,7,8]}],[{"useCaseName":"overloadProtectionByEvChargingCurrentCurtailment"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1,2,3]}],[{"useCaseName":"evCommissioningAndConfiguration"},{"useCaseVersion":"1.0.1"},{"scenarioSupport":[1,2,3,4,5,6,7,8]}]]}]]}]}]]}]` 78 | if jsonString != jsonTest { 79 | fmt.Println("EXPECTED:") 80 | fmt.Println(string(jsonTest)) 81 | fmt.Println("\nACTUAL:") 82 | fmt.Println(string(jsonString)) 83 | 84 | t.Errorf("TestUseCaseInformationReply() actual json string doesn't match expected result") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /spine/model/version.go: -------------------------------------------------------------------------------- 1 | // Package ns_p contains models for http://docs.eebus.org/spine/xsd/v1 2 | package model 3 | 4 | // Code generated by github.com/andig/xsd2go. DO NOT EDIT. 5 | 6 | import "github.com/evcc-io/eebus/util" 7 | 8 | // SpecificationVersionDataType complex type 9 | type SpecificationVersionDataType SpecificationVersionType 10 | 11 | // SpecificationVersionListDataType complex type 12 | type SpecificationVersionListDataType struct { 13 | SpecificationVersionData []SpecificationVersionDataType `json:"specificationVersionData,omitempty"` 14 | } 15 | 16 | // MarshalJSON is the SHIP serialization marshaller 17 | func (m SpecificationVersionListDataType) MarshalJSON() ([]byte, error) { 18 | return util.Marshal(m) 19 | } 20 | 21 | // UnmarshalJSON is the SHIP serialization unmarshaller 22 | func (m *SpecificationVersionListDataType) UnmarshalJSON(data []byte) error { 23 | return util.Unmarshal(data, &m) 24 | } 25 | 26 | // SpecificationVersionListDataSelectorsType complex type 27 | type SpecificationVersionListDataSelectorsType struct { 28 | } 29 | 30 | // MarshalJSON is the SHIP serialization marshaller 31 | func (m SpecificationVersionListDataSelectorsType) MarshalJSON() ([]byte, error) { 32 | return util.Marshal(m) 33 | } 34 | 35 | // UnmarshalJSON is the SHIP serialization unmarshaller 36 | func (m *SpecificationVersionListDataSelectorsType) UnmarshalJSON(data []byte) error { 37 | return util.Unmarshal(data, &m) 38 | } 39 | -------------------------------------------------------------------------------- /spine/rw.go: -------------------------------------------------------------------------------- 1 | package spine 2 | 3 | import ( 4 | "github.com/evcc-io/eebus/spine/model" 5 | ) 6 | 7 | type RW struct { 8 | Read, Write bool 9 | } 10 | 11 | func (rw RW) String() string { 12 | switch { 13 | case rw.Read && !rw.Write: 14 | return "RO" 15 | case rw.Read && rw.Write: 16 | return "RW" 17 | default: 18 | return "--" 19 | } 20 | } 21 | 22 | func (rw RW) Information() *model.PossibleOperationsType { 23 | res := new(model.PossibleOperationsType) 24 | if rw.Read { 25 | res.Read = &model.PossibleOperationsReadType{} 26 | } 27 | if rw.Write { 28 | res.Write = &model.PossibleOperationsWriteType{} 29 | } 30 | 31 | return res 32 | } 33 | -------------------------------------------------------------------------------- /util/log.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | type Logger interface { 9 | Printf(format string, v ...interface{}) 10 | Println(v ...interface{}) 11 | } 12 | 13 | type NopLogger struct{} 14 | 15 | func (l *NopLogger) Printf(format string, v ...interface{}) {} 16 | 17 | func (l *NopLogger) Println(v ...interface{}) {} 18 | 19 | type LogWriter struct { 20 | io.Writer 21 | TimeFormat string 22 | } 23 | 24 | func (w LogWriter) Write(b []byte) (n int, err error) { 25 | return w.Writer.Write(append([]byte(time.Now().Format(w.TimeFormat)), b...)) 26 | } 27 | -------------------------------------------------------------------------------- /util/marshal.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/fatih/structs" 10 | ) 11 | 12 | // Marshal is the SHIP serialization 13 | func Marshal(v interface{}) ([]byte, error) { 14 | e := make([]map[string]interface{}, 0) 15 | 16 | for _, f := range structs.Fields(v) { 17 | if !f.IsExported() { 18 | continue 19 | } 20 | 21 | jsonTag := f.Tag("json") 22 | if f.IsZero() && strings.HasSuffix(jsonTag, ",omitempty") { 23 | continue 24 | } 25 | 26 | key := f.Name() 27 | if jsonTag != "" { 28 | key = strings.TrimSuffix(jsonTag, ",omitempty") 29 | } 30 | 31 | m := map[string]interface{}{key: f.Value()} 32 | e = append(e, m) 33 | } 34 | 35 | return json.Marshal(e) 36 | } 37 | 38 | // Unmarshal is the SHIP de-serialization 39 | func Unmarshal(data []byte, v interface{}) error { 40 | var ar []map[string]json.RawMessage 41 | 42 | // convert input to json array 43 | if data[0] != byte('[') { 44 | data = append([]byte{'['}, append(data, ']')...) 45 | } 46 | if err := json.Unmarshal(data, &ar); err != nil { 47 | return err 48 | } 49 | 50 | // convert array elements to struct members 51 | for _, ae := range ar { 52 | if len(ae) > 1 { 53 | return fmt.Errorf("unmarshal: invalid map %v", ae) 54 | } 55 | 56 | // extract 1-element map 57 | var key string 58 | var val json.RawMessage 59 | for k, v := range ae { 60 | key = k 61 | val = v 62 | } 63 | 64 | // fmt.Println("json:", string(val)) 65 | 66 | // find field 67 | var field *structs.Field 68 | for _, f := range structs.Fields(v) { 69 | name := f.Name() 70 | if jsonTag := f.Tag("json"); jsonTag != "" { 71 | name = strings.TrimSuffix(jsonTag, ",omitempty") 72 | } 73 | 74 | if name == key { 75 | field = f 76 | break 77 | } 78 | } 79 | 80 | if field == nil { 81 | return fmt.Errorf("unmarshal: field not found: %s", key) 82 | } 83 | 84 | // convert value into pointer to value as interface 85 | iface := reflect.New(reflect.TypeOf(field.Value())).Interface() 86 | 87 | // use pointer-interface to unmarshal into target type 88 | if err := json.Unmarshal(val, iface); err != nil { 89 | return err 90 | } 91 | 92 | // de-reference 93 | iface = reflect.ValueOf(iface).Elem().Interface() 94 | 95 | // fmt.Printf("set: %s=%+v (%T)\n", field.Name(), iface, iface) 96 | if err := field.Set(iface); err != nil { 97 | // fmt.Printf("err: %v\n", err) 98 | return err 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | --------------------------------------------------------------------------------