• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

enbility / spine-go / 13037019031

29 Jan 2025 05:31PM UTC coverage: 93.688% (+0.004%) from 93.684%
13037019031

Pull #51

github

web-flow
Merge 89e9a5f71 into 5fb9ea129
Pull Request #51: Various maintenance tasks

5061 of 5402 relevant lines covered (93.69%)

88.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.5
/spine/device_local.go
1
package spine
2

3
import (
4
        "errors"
5
        "fmt"
6
        "reflect"
7
        "slices"
8
        "sync"
9

10
        shipapi "github.com/enbility/ship-go/api"
11
        "github.com/enbility/ship-go/logging"
12
        "github.com/enbility/spine-go/api"
13
        "github.com/enbility/spine-go/model"
14
        "github.com/enbility/spine-go/util"
15
)
16

17
type DeviceLocal struct {
18
        *Device
19
        entities            []api.EntityLocalInterface
20
        subscriptionManager api.SubscriptionManagerInterface
21
        bindingManager      api.BindingManagerInterface
22
        nodeManagement      *NodeManagement
23

24
        remoteDevices map[string]api.DeviceRemoteInterface
25

26
        brandName    string
27
        deviceModel  string
28
        deviceCode   string
29
        serialNumber string
30

31
        mux sync.Mutex
32
}
33

34
// BrandName is the brand
35
// DeviceModel is the model
36
// SerialNumber is the serial number
37
// DeviceCode is the SHIP id (accessMethods.id)
38
// DeviceAddress is the SPINE device address
39
func NewDeviceLocal(
40
        brandName, deviceModel, serialNumber, deviceCode, deviceAddress string,
41
        deviceType model.DeviceTypeType,
42
        featureSet model.NetworkManagementFeatureSetType) *DeviceLocal {
46✔
43
        address := model.AddressDeviceType(deviceAddress)
46✔
44

46✔
45
        var fSet *model.NetworkManagementFeatureSetType
46✔
46
        if len(featureSet) != 0 {
92✔
47
                fSet = &featureSet
46✔
48
        }
46✔
49

50
        res := &DeviceLocal{
46✔
51
                Device:        NewDevice(&address, &deviceType, fSet),
46✔
52
                remoteDevices: make(map[string]api.DeviceRemoteInterface),
46✔
53
                brandName:     brandName,
46✔
54
                deviceModel:   deviceModel,
46✔
55
                serialNumber:  serialNumber,
46✔
56
                deviceCode:    deviceCode,
46✔
57
        }
46✔
58

46✔
59
        res.subscriptionManager = NewSubscriptionManager(res)
46✔
60
        res.bindingManager = NewBindingManager(res)
46✔
61

46✔
62
        res.addDeviceInformation()
46✔
63
        return res
46✔
64
}
65

66
var _ api.EventHandlerInterface = (*DeviceLocal)(nil)
67

68
/* EventHandlerInterface */
69

70
// React to some specific events
71
func (r *DeviceLocal) HandleEvent(payload api.EventPayload) {
368✔
72
        // Subscribe to NodeManagement after DetailedDiscovery is received
368✔
73
        if payload.EventType != api.EventTypeDeviceChange || payload.ChangeType != api.ElementChangeAdd {
691✔
74
                return
323✔
75
        }
323✔
76

77
        if payload.Data == nil {
57✔
78
                return
12✔
79
        }
12✔
80

81
        if len(payload.Ski) == 0 {
33✔
82
                return
×
83
        }
×
84

85
        remoteDevice := r.RemoteDeviceForSki(payload.Ski)
33✔
86
        if remoteDevice == nil {
57✔
87
                return
24✔
88
        }
24✔
89

90
        // the codefactor warning is invalid, as .(type) check can not be replaced with if then
91
        //revive:disable-next-line
92
        switch payload.Data.(type) {
9✔
93
        case *model.NodeManagementDetailedDiscoveryDataType:
9✔
94
                address := payload.Feature.Address()
9✔
95
                if address.Device == nil {
15✔
96
                        address.Device = remoteDevice.Address()
6✔
97
                }
6✔
98
                _, _ = r.nodeManagement.SubscribeToRemote(address)
9✔
99

9✔
100
                // Request Use Case Data
9✔
101
                _, _ = r.nodeManagement.RequestUseCaseData(payload.Device.Ski(), remoteDevice.Address(), payload.Device.Sender())
9✔
102
        }
103
}
104

105
var _ api.DeviceLocalInterface = (*DeviceLocal)(nil)
106

107
/* DeviceLocalInterface */
108

109
// Setup a new remote device with a given SKI and triggers SPINE requesting device details
110
func (r *DeviceLocal) SetupRemoteDevice(ski string, writeI shipapi.ShipConnectionDataWriterInterface) shipapi.ShipConnectionDataReaderInterface {
20✔
111
        sender := NewSender(writeI)
20✔
112
        rDevice := NewDeviceRemote(r, ski, sender)
20✔
113

20✔
114
        r.AddRemoteDeviceForSki(ski, rDevice)
20✔
115

20✔
116
        // always add subscription, as it checks if it already exists
20✔
117
        _ = Events.subscribe(api.EventHandlerLevelCore, r)
20✔
118

20✔
119
        // Request Detailed Discovery Data
20✔
120
        _, _ = r.RequestRemoteDetailedDiscoveryData(rDevice)
20✔
121

20✔
122
        // TODO: Add error handling
20✔
123
        // If the request returned an error, it should be retried until it does not
20✔
124

20✔
125
        return rDevice
20✔
126
}
20✔
127

128
func (r *DeviceLocal) RequestRemoteDetailedDiscoveryData(rDevice api.DeviceRemoteInterface) (*model.MsgCounterType, *model.ErrorType) {
20✔
129
        // Request Detailed Discovery Data
20✔
130
        return r.nodeManagement.RequestDetailedDiscovery(rDevice.Ski(), rDevice.Address(), rDevice.Sender())
20✔
131
}
20✔
132

133
// Helper method used by tests and AddRemoteDevice
134
func (r *DeviceLocal) AddRemoteDeviceForSki(ski string, rDevice api.DeviceRemoteInterface) {
25✔
135
        r.mux.Lock()
25✔
136
        defer r.mux.Unlock()
25✔
137

25✔
138
        r.remoteDevices[ski] = rDevice
25✔
139
}
25✔
140

141
func (r *DeviceLocal) RemoveRemoteDeviceConnection(ski string) {
2✔
142
        remoteDevice := r.RemoteDeviceForSki(ski)
2✔
143

2✔
144
        // we get the events for any disconnection, even for cases where SHIP
2✔
145
        // closed a connection and therefor it never reached SPINE
2✔
146
        if remoteDevice == nil {
3✔
147
                return
1✔
148
        }
1✔
149

150
        r.RemoveRemoteDevice(ski)
1✔
151

1✔
152
        // inform about the disconnection
1✔
153
        payload := api.EventPayload{
1✔
154
                Ski:        ski,
1✔
155
                EventType:  api.EventTypeDeviceChange,
1✔
156
                ChangeType: api.ElementChangeRemove,
1✔
157
                Device:     remoteDevice,
1✔
158
        }
1✔
159
        Events.Publish(payload)
1✔
160
}
161

162
func (r *DeviceLocal) RemoveRemoteDevice(ski string) {
2✔
163
        remoteDevice := r.RemoteDeviceForSki(ski)
2✔
164
        if remoteDevice == nil {
2✔
165
                return
×
166
        }
×
167

168
        // remove all subscriptions for this device
169
        subscriptionMgr := r.SubscriptionManager()
2✔
170
        subscriptionMgr.RemoveSubscriptionsForDevice(remoteDevice)
2✔
171

2✔
172
        // remove all bindings for this device
2✔
173
        bindingMgr := r.BindingManager()
2✔
174
        bindingMgr.RemoveBindingsForDevice(remoteDevice)
2✔
175

2✔
176
        r.mux.Lock()
2✔
177

2✔
178
        delete(r.remoteDevices, ski)
2✔
179

2✔
180
        // only unsubscribe if we don't have any remote devices left
2✔
181
        if len(r.remoteDevices) == 0 {
4✔
182
                _ = Events.unsubscribe(api.EventHandlerLevelCore, r)
2✔
183
        }
2✔
184

185
        r.mux.Unlock()
2✔
186

2✔
187
        remoteDeviceAddress := &model.DeviceAddressType{
2✔
188
                Device: remoteDevice.Address(),
2✔
189
        }
2✔
190
        // remove all data caches for this device
2✔
191
        for _, entity := range r.entities {
4✔
192
                for _, feature := range entity.Features() {
6✔
193
                        feature.CleanWriteApprovalCaches(ski)
4✔
194
                        feature.CleanRemoteDeviceCaches(remoteDeviceAddress)
4✔
195
                }
4✔
196
        }
197
}
198

199
func (r *DeviceLocal) RemoteDevices() []api.DeviceRemoteInterface {
2✔
200
        r.mux.Lock()
2✔
201
        defer r.mux.Unlock()
2✔
202

2✔
203
        res := make([]api.DeviceRemoteInterface, 0)
2✔
204
        for _, rDevice := range r.remoteDevices {
3✔
205
                res = append(res, rDevice)
1✔
206
        }
1✔
207

208
        return res
2✔
209
}
210

211
func (r *DeviceLocal) RemoteDeviceForAddress(address model.AddressDeviceType) api.DeviceRemoteInterface {
32✔
212
        r.mux.Lock()
32✔
213
        defer r.mux.Unlock()
32✔
214

32✔
215
        for _, item := range r.remoteDevices {
62✔
216
                if item.Address() != nil && *item.Address() == address {
56✔
217
                        return item
26✔
218
                }
26✔
219
        }
220

221
        return nil
6✔
222
}
223

224
func (r *DeviceLocal) RemoteDeviceForSki(ski string) api.DeviceRemoteInterface {
58✔
225
        r.mux.Lock()
58✔
226
        defer r.mux.Unlock()
58✔
227

58✔
228
        return r.remoteDevices[ski]
58✔
229
}
58✔
230

231
func (r *DeviceLocal) AddEntity(entity api.EntityLocalInterface) {
36✔
232
        r.mux.Lock()
36✔
233

36✔
234
        r.entities = append(r.entities, entity)
36✔
235

36✔
236
        r.mux.Unlock()
36✔
237

36✔
238
        r.notifySubscribersOfEntity(entity, model.NetworkManagementStateChangeTypeAdded)
36✔
239
}
36✔
240

241
func (r *DeviceLocal) RemoveEntity(entity api.EntityLocalInterface) {
3✔
242
        entity.RemoveAllUseCaseSupports()
3✔
243
        entity.RemoveAllSubscriptions()
3✔
244
        entity.RemoveAllBindings()
3✔
245

3✔
246
        if heartbeatMgr := entity.HeartbeatManager(); heartbeatMgr != nil {
6✔
247
                heartbeatMgr.StopHeartbeat()
3✔
248
        }
3✔
249

250
        r.mux.Lock()
3✔
251

3✔
252
        var entities []api.EntityLocalInterface
3✔
253
        for _, e := range r.entities {
10✔
254
                if e != entity {
11✔
255
                        entities = append(entities, e)
4✔
256
                }
4✔
257
        }
258

259
        r.entities = entities
3✔
260

3✔
261
        r.mux.Unlock()
3✔
262

3✔
263
        r.notifySubscribersOfEntity(entity, model.NetworkManagementStateChangeTypeRemoved)
3✔
264
}
265

266
func (r *DeviceLocal) Entities() []api.EntityLocalInterface {
6✔
267
        r.mux.Lock()
6✔
268
        defer r.mux.Unlock()
6✔
269

6✔
270
        return r.entities
6✔
271
}
6✔
272

273
func (r *DeviceLocal) Entity(id []model.AddressEntityType) api.EntityLocalInterface {
60✔
274
        r.mux.Lock()
60✔
275
        defer r.mux.Unlock()
60✔
276

60✔
277
        for _, e := range r.entities {
162✔
278
                if reflect.DeepEqual(id, e.Address().Entity) {
157✔
279
                        return e
55✔
280
                }
55✔
281
        }
282
        return nil
5✔
283
}
284

285
func (r *DeviceLocal) EntityForType(entityType model.EntityTypeType) api.EntityLocalInterface {
×
286
        r.mux.Lock()
×
287
        defer r.mux.Unlock()
×
288

×
289
        for _, e := range r.entities {
×
290
                if e.EntityType() == entityType {
×
291
                        return e
×
292
                }
×
293
        }
294
        return nil
×
295
}
296

297
func (r *DeviceLocal) FeatureByAddress(address *model.FeatureAddressType) api.FeatureLocalInterface {
58✔
298
        entity := r.Entity(address.Entity)
58✔
299
        if entity != nil {
112✔
300
                return entity.FeatureOfAddress(address.Feature)
54✔
301
        }
54✔
302
        return nil
4✔
303
}
304

305
func (r *DeviceLocal) CleanRemoteEntityCaches(remoteAddress *model.EntityAddressType) {
2✔
306
        for _, entity := range r.entities {
4✔
307
                for _, feature := range entity.Features() {
6✔
308
                        feature.CleanRemoteEntityCaches(remoteAddress)
4✔
309
                }
4✔
310
        }
311
}
312

313
func (r *DeviceLocal) ProcessCmd(datagram model.DatagramType, remoteDevice api.DeviceRemoteInterface) error {
25✔
314
        destAddr := datagram.Header.AddressDestination
25✔
315
        localFeature := r.FeatureByAddress(destAddr)
25✔
316

25✔
317
        cmdClassifier := datagram.Header.CmdClassifier
25✔
318
        if len(datagram.Payload.Cmd) == 0 {
27✔
319
                return errors.New("no payload cmd content available")
2✔
320
        }
2✔
321
        cmd := datagram.Payload.Cmd[0]
23✔
322

23✔
323
        // TODO check if cmd.Function is the same as the provided cmd value
23✔
324
        filterPartial, filterDelete := cmd.ExtractFilter()
23✔
325

23✔
326
        remoteEntity := remoteDevice.Entity(datagram.Header.AddressSource.Entity)
23✔
327
        remoteFeature := remoteDevice.FeatureByAddress(datagram.Header.AddressSource)
23✔
328
        if remoteFeature == nil {
24✔
329
                return fmt.Errorf("invalid remote feature address: '%s'", datagram.Header.AddressSource)
1✔
330
        }
1✔
331

332
        message := &api.Message{
22✔
333
                RequestHeader: &datagram.Header,
22✔
334
                Cmd:           cmd,
22✔
335
                FilterPartial: filterPartial,
22✔
336
                FilterDelete:  filterDelete,
22✔
337
                FeatureRemote: remoteFeature,
22✔
338
                EntityRemote:  remoteEntity,
22✔
339
                DeviceRemote:  remoteDevice,
22✔
340
        }
22✔
341

22✔
342
        if cmdClassifier != nil {
43✔
343
                message.CmdClassifier = *cmdClassifier
21✔
344
        } else {
22✔
345
                errorMessage := "cmdClassifier may not be empty"
1✔
346

1✔
347
                _ = remoteFeature.Device().Sender().ResultError(message.RequestHeader, destAddr, model.NewErrorType(model.ErrorNumberTypeDestinationUnknown, errorMessage))
1✔
348

1✔
349
                return errors.New(errorMessage)
1✔
350
        }
1✔
351

352
        if localFeature == nil {
22✔
353
                errorMessage := "invalid feature address"
1✔
354
                _ = remoteFeature.Device().Sender().ResultError(message.RequestHeader, destAddr, model.NewErrorType(model.ErrorNumberTypeDestinationUnknown, errorMessage))
1✔
355

1✔
356
                return errors.New(errorMessage)
1✔
357
        }
1✔
358

359
        lfType := string(localFeature.Type())
20✔
360
        rfType := string(remoteFeature.Type())
20✔
361

20✔
362
        logging.Log().Debug(datagram.PrintMessageOverview(false, lfType, rfType))
20✔
363

20✔
364
        // check if this is a write with an existing binding and if write is allowed on this feature
20✔
365
        if message.CmdClassifier == model.CmdClassifierTypeWrite {
22✔
366
                cmdData, err := cmd.Data()
2✔
367
                if err != nil || cmdData.Function == nil {
2✔
368
                        err := model.NewErrorTypeFromString("no function found for cmd data")
×
369
                        _ = remoteFeature.Device().Sender().ResultError(message.RequestHeader, localFeature.Address(), err)
×
370
                        return errors.New(err.String())
×
371
                }
×
372

373
                if operations, ok := localFeature.Operations()[*cmdData.Function]; !ok || !operations.Write() {
3✔
374
                        err := model.NewErrorTypeFromString("write is not allowed on this function")
1✔
375
                        _ = remoteFeature.Device().Sender().ResultError(message.RequestHeader, localFeature.Address(), err)
1✔
376
                        return errors.New(err.String())
1✔
377
                }
1✔
378

379
                if !r.BindingManager().HasLocalFeatureRemoteBinding(localFeature.Address(), remoteFeature.Address()) {
2✔
380
                        err := model.NewErrorTypeFromString("write denied due to missing binding")
1✔
381
                        _ = remoteFeature.Device().Sender().ResultError(message.RequestHeader, localFeature.Address(), err)
1✔
382
                        return errors.New(err.String())
1✔
383
                }
1✔
384
        }
385

386
        err := localFeature.HandleMessage(message)
18✔
387
        if err != nil {
20✔
388
                // TODO: add error description in a useful format
2✔
389

2✔
390
                // Don't send error responses for incoming resulterror messages
2✔
391
                if message.CmdClassifier != model.CmdClassifierTypeResult {
4✔
392
                        _ = remoteFeature.Device().Sender().ResultError(message.RequestHeader, localFeature.Address(), err)
2✔
393
                }
2✔
394

395
                // if this is an error for a notify message, automatically trigger a read request for the same
396
                if message.CmdClassifier == model.CmdClassifierTypeNotify {
3✔
397
                        // set the command function to empty
1✔
398

1✔
399
                        if cmdData, err := message.Cmd.Data(); err == nil {
2✔
400
                                _, _ = localFeature.RequestRemoteData(*cmdData.Function, nil, nil, remoteFeature)
1✔
401
                        }
1✔
402
                }
403

404
                return errors.New(err.String())
2✔
405
        }
406

407
        ackRequest := message.RequestHeader.AckRequest
16✔
408
        ackClassifiers := []model.CmdClassifierType{
16✔
409
                model.CmdClassifierTypeCall,
16✔
410
                model.CmdClassifierTypeReply,
16✔
411
                model.CmdClassifierTypeNotify}
16✔
412

16✔
413
        if ackRequest != nil && *ackRequest && slices.Contains(ackClassifiers, message.CmdClassifier) {
19✔
414
                // return success as defined in SPINE chapter 5.2.4
3✔
415
                _ = remoteFeature.Device().Sender().ResultSuccess(message.RequestHeader, localFeature.Address())
3✔
416
        }
3✔
417

418
        return nil
16✔
419
}
420

421
func (r *DeviceLocal) NodeManagement() api.NodeManagementInterface {
20✔
422
        return r.nodeManagement
20✔
423
}
20✔
424

425
func (r *DeviceLocal) SubscriptionManager() api.SubscriptionManagerInterface {
127✔
426
        return r.subscriptionManager
127✔
427
}
127✔
428

429
func (r *DeviceLocal) BindingManager() api.BindingManagerInterface {
10✔
430
        return r.bindingManager
10✔
431
}
10✔
432

433
func (r *DeviceLocal) Information() *model.NodeManagementDetailedDiscoveryDeviceInformationType {
41✔
434
        res := model.NodeManagementDetailedDiscoveryDeviceInformationType{
41✔
435
                Description: &model.NetworkManagementDeviceDescriptionDataType{
41✔
436
                        DeviceAddress: &model.DeviceAddressType{
41✔
437
                                Device: r.address,
41✔
438
                        },
41✔
439
                        DeviceType:        r.dType,
41✔
440
                        NetworkFeatureSet: r.featureSet,
41✔
441
                },
41✔
442
        }
41✔
443
        return &res
41✔
444
}
41✔
445

446
func (r *DeviceLocal) NotifySubscribers(featureAddress *model.FeatureAddressType, cmd model.CmdType) {
110✔
447
        subscriptions := r.SubscriptionManager().SubscriptionsOnFeature(*featureAddress)
110✔
448
        for _, subscription := range subscriptions {
123✔
449
                // TODO: error handling
13✔
450
                _, _ = subscription.ClientFeature.Device().Sender().Notify(subscription.ServerFeature.Address(), subscription.ClientFeature.Address(), cmd)
13✔
451
        }
13✔
452
}
453

454
func (r *DeviceLocal) notifySubscribersOfEntity(entity api.EntityLocalInterface, state model.NetworkManagementStateChangeType) {
39✔
455
        deviceInformation := r.Information()
39✔
456
        entityInformation := *entity.Information()
39✔
457
        entityInformation.Description.LastStateChange = &state
39✔
458

39✔
459
        var featureInformation []model.NodeManagementDetailedDiscoveryFeatureInformationType
39✔
460
        if state == model.NetworkManagementStateChangeTypeAdded {
75✔
461
                for _, f := range entity.Features() {
38✔
462
                        featureInformation = append(featureInformation, *f.Information())
2✔
463
                }
2✔
464
        }
465

466
        cmd := model.CmdType{
39✔
467
                Function: util.Ptr(model.FunctionTypeNodeManagementDetailedDiscoveryData),
39✔
468
                Filter:   filterEmptyPartial(),
39✔
469
                NodeManagementDetailedDiscoveryData: &model.NodeManagementDetailedDiscoveryDataType{
39✔
470
                        SpecificationVersionList: &model.NodeManagementSpecificationVersionListType{
39✔
471
                                SpecificationVersion: []model.SpecificationVersionDataType{model.SpecificationVersionDataType(SpecificationVersion)},
39✔
472
                        },
39✔
473
                        DeviceInformation:  deviceInformation,
39✔
474
                        EntityInformation:  []model.NodeManagementDetailedDiscoveryEntityInformationType{entityInformation},
39✔
475
                        FeatureInformation: featureInformation,
39✔
476
                },
39✔
477
        }
39✔
478

39✔
479
        r.NotifySubscribers(r.nodeManagement.Address(), cmd)
39✔
480
}
481

482
func (r *DeviceLocal) addDeviceInformation() {
46✔
483
        entityType := model.EntityTypeTypeDeviceInformation
46✔
484
        entity := NewEntityLocal(r, entityType, []model.AddressEntityType{model.AddressEntityType(DeviceInformationEntityId)}, 0)
46✔
485

46✔
486
        {
92✔
487
                r.nodeManagement = NewNodeManagement(entity.NextFeatureId(), entity)
46✔
488
                entity.AddFeature(r.nodeManagement)
46✔
489
        }
46✔
490
        {
46✔
491
                f := NewFeatureLocal(entity.NextFeatureId(), entity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer)
46✔
492

46✔
493
                f.AddFunctionType(model.FunctionTypeDeviceClassificationManufacturerData, true, false)
46✔
494

46✔
495
                manufacturerData := &model.DeviceClassificationManufacturerDataType{
46✔
496
                        BrandName:    util.Ptr(model.DeviceClassificationStringType(r.brandName)),
46✔
497
                        VendorName:   util.Ptr(model.DeviceClassificationStringType(r.brandName)),
46✔
498
                        DeviceName:   util.Ptr(model.DeviceClassificationStringType(r.deviceModel)),
46✔
499
                        DeviceCode:   util.Ptr(model.DeviceClassificationStringType(r.deviceCode)),
46✔
500
                        SerialNumber: util.Ptr(model.DeviceClassificationStringType(r.serialNumber)),
46✔
501
                }
46✔
502
                f.SetData(model.FunctionTypeDeviceClassificationManufacturerData, manufacturerData)
46✔
503

46✔
504
                entity.AddFeature(f)
46✔
505
        }
46✔
506

507
        r.entities = append(r.entities, entity)
46✔
508
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc