diff --git a/spine/device.go b/spine/device.go index 97ad813e..b4944619 100644 --- a/spine/device.go +++ b/spine/device.go @@ -3,10 +3,9 @@ package spine import "github.com/enbility/eebus-go/spine/model" type DeviceImpl struct { - address *model.AddressDeviceType - dType *model.DeviceTypeType - featureSet *model.NetworkManagementFeatureSetType - useCaseManager *UseCaseManager + address *model.AddressDeviceType + dType *model.DeviceTypeType + featureSet *model.NetworkManagementFeatureSetType } // Initialize a new device @@ -15,8 +14,6 @@ type DeviceImpl struct { func NewDeviceImpl(address *model.AddressDeviceType, dType *model.DeviceTypeType, featureSet *model.NetworkManagementFeatureSetType) *DeviceImpl { deviceImpl := &DeviceImpl{} - deviceImpl.useCaseManager = NewUseCaseManager(deviceImpl) - if dType != nil { deviceImpl.dType = dType } @@ -36,10 +33,6 @@ func (r *DeviceImpl) Address() *model.AddressDeviceType { return r.address } -func (r *DeviceImpl) UseCaseManager() *UseCaseManager { - return r.useCaseManager -} - func (r *DeviceImpl) DeviceType() *model.DeviceTypeType { return r.dType } diff --git a/spine/device_local.go b/spine/device_local.go index df5647f6..65b57e07 100644 --- a/spine/device_local.go +++ b/spine/device_local.go @@ -281,6 +281,7 @@ func (r *DeviceLocalImpl) AddEntity(entity *EntityLocalImpl) { } func (r *DeviceLocalImpl) RemoveEntity(entity *EntityLocalImpl) { + entity.RemoveAllUseCaseSupports() entity.RemoveAllSubscriptions() entity.RemoveAllBindings() diff --git a/spine/device_remote.go b/spine/device_remote.go index 53656c37..16003b0f 100644 --- a/spine/device_remote.go +++ b/spine/device_remote.go @@ -243,15 +243,18 @@ func (d *DeviceRemoteImpl) VerifyUseCaseScenariosAndFeaturesSupport( scenarios []model.UseCaseScenarioSupportType, serverFeatures []model.FeatureTypeType, ) bool { - remoteUseCaseManager := d.UseCaseManager() + entity := d.Entity(DeviceInformationAddressEntity) - usecases := remoteUseCaseManager.UseCaseInformation() - if len(usecases) == 0 { + nodemgmt := d.FeatureByEntityTypeAndRole(entity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + + usecases := nodemgmt.Data(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) + + if usecases == nil || len(usecases.UseCaseInformation) == 0 { return false } usecaseAndScenariosFound := false - for _, usecase := range usecases { + for _, usecase := range usecases.UseCaseInformation { if usecase.Actor == nil || *usecase.Actor != usecaseActor { continue } @@ -261,10 +264,6 @@ func (d *DeviceRemoteImpl) VerifyUseCaseScenariosAndFeaturesSupport( continue } - if support.UseCaseAvailable == nil || !*support.UseCaseAvailable { - continue - } - var foundScenarios []model.UseCaseScenarioSupportType for _, scenario := range support.ScenarioSupport { if slices.Contains(scenarios, scenario) { diff --git a/spine/device_remote_test.go b/spine/device_remote_test.go index 1e0b14ea..d7eb535f 100644 --- a/spine/device_remote_test.go +++ b/spine/device_remote_test.go @@ -5,10 +5,15 @@ import ( "time" "github.com/enbility/eebus-go/spine/model" + "github.com/enbility/eebus-go/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) +const ( + nm_usecaseinformationlistdata_recv_reply_file_path = "../spine/testdata/nm_usecaseinformationlistdata_recv_reply.json" +) + func TestDeviceRemoteSuite(t *testing.T) { suite.Run(t, new(DeviceRemoteSuite)) } @@ -18,6 +23,7 @@ type DeviceRemoteSuite struct { localDevice *DeviceLocalImpl remoteDevice *DeviceRemoteImpl + remoteEntity *EntityRemoteImpl } func (s *DeviceRemoteSuite) WriteSpineMessage([]byte) {} @@ -30,14 +36,15 @@ func (s *DeviceRemoteSuite) BeforeTest(suiteName, testName string) { ski := "test" sender := NewSender(s) s.remoteDevice = NewDeviceRemoteImpl(s.localDevice, ski, sender) + s.remoteDevice.address = util.Ptr(model.AddressDeviceType("test")) s.localDevice.AddRemoteDevice(ski, s) - entity := NewEntityRemoteImpl(s.remoteDevice, model.EntityTypeTypeEVSE, []model.AddressEntityType{1}) + s.remoteEntity = NewEntityRemoteImpl(s.remoteDevice, model.EntityTypeTypeEVSE, []model.AddressEntityType{1}) - feature := NewFeatureRemoteImpl(0, entity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) - entity.AddFeature(feature) + feature := NewFeatureRemoteImpl(0, s.remoteEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + s.remoteEntity.AddFeature(feature) - s.remoteDevice.AddEntity(entity) + s.remoteDevice.AddEntity(s.remoteEntity) } func (s *DeviceRemoteSuite) Test_RemoveByAddress() { @@ -73,6 +80,26 @@ func (s *DeviceRemoteSuite) Test_FeatureByEntityTypeAndRole() { assert.Nil(s.T(), feature) } +func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport_ElliJSON() { + _, _ = s.remoteDevice.HandleIncomingSpineMesssage(loadFileData(s.T(), nm_usecaseinformationlistdata_recv_reply_file_path)) + + result := s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeBatterySystem, + model.UseCaseNameTypeControlOfBattery, + []model.UseCaseScenarioSupportType{}, + []model.FeatureTypeType{}, + ) + assert.Equal(s.T(), false, result) + + result = s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEVSE, + model.UseCaseNameTypeEVSECommissioningAndConfiguration, + []model.UseCaseScenarioSupportType{}, + []model.FeatureTypeType{}, + ) + assert.Equal(s.T(), true, result) +} + func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { result := s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( model.UseCaseActorTypeEVSE, @@ -82,7 +109,24 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { ) assert.Equal(s.T(), false, result) - s.remoteDevice.UseCaseManager().Add( + nodeMgmtEntity := s.remoteDevice.Entity(DeviceInformationAddressEntity) + nodeMgmt := nodeMgmtEntity.Feature(util.Ptr(model.AddressFeatureType(NodeManagementFeatureId))) + + // initialize with empty data + newData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{}, + } + nodeMgmt.UpdateData(model.FunctionTypeNodeManagementUseCaseData, newData, nil, nil) + + data := nodeMgmt.Data(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) + + address := model.FeatureAddressType{ + Device: s.remoteDevice.address, + Entity: s.remoteEntity.address.Entity, + } + + data.AddUseCaseSupport( + address, model.UseCaseActorTypeBatterySystem, model.UseCaseNameTypeControlOfBattery, model.SpecificationVersionType("1.0.0"), @@ -90,16 +134,19 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { true, []model.UseCaseScenarioSupportType{1}, ) + nodeMgmt.SetData(model.FunctionTypeNodeManagementUseCaseData, data) + data = nodeMgmt.Data(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) result = s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, - []model.UseCaseScenarioSupportType{}, - []model.FeatureTypeType{}, + nil, + nil, ) assert.Equal(s.T(), false, result) - s.remoteDevice.UseCaseManager().Add( + data.AddUseCaseSupport( + address, model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVCommissioningAndConfiguration, model.SpecificationVersionType("1.0.0"), @@ -107,16 +154,19 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { true, []model.UseCaseScenarioSupportType{1}, ) + nodeMgmt.SetData(model.FunctionTypeNodeManagementUseCaseData, data) + data = nodeMgmt.Data(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) result = s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, - []model.UseCaseScenarioSupportType{}, - []model.FeatureTypeType{}, + nil, + nil, ) assert.Equal(s.T(), false, result) - s.remoteDevice.UseCaseManager().Add( + data.AddUseCaseSupport( + address, model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, model.SpecificationVersionType("1.0.0"), @@ -124,16 +174,19 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { false, []model.UseCaseScenarioSupportType{1}, ) + nodeMgmt.SetData(model.FunctionTypeNodeManagementUseCaseData, data) + data = nodeMgmt.Data(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) result = s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, - []model.UseCaseScenarioSupportType{}, - []model.FeatureTypeType{}, + nil, + nil, ) - assert.Equal(s.T(), false, result) + assert.Equal(s.T(), true, result) - s.remoteDevice.UseCaseManager().Add( + data.AddUseCaseSupport( + address, model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, model.SpecificationVersionType("1.0.0"), @@ -141,12 +194,13 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { true, []model.UseCaseScenarioSupportType{1}, ) + nodeMgmt.SetData(model.FunctionTypeNodeManagementUseCaseData, data) result = s.remoteDevice.VerifyUseCaseScenariosAndFeaturesSupport( model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, - []model.UseCaseScenarioSupportType{}, - []model.FeatureTypeType{}, + nil, + nil, ) assert.Equal(s.T(), true, result) @@ -154,7 +208,7 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, []model.UseCaseScenarioSupportType{2}, - []model.FeatureTypeType{}, + nil, ) assert.Equal(s.T(), false, result) @@ -162,7 +216,7 @@ func (s *DeviceRemoteSuite) Test_VerifyUseCaseScenariosAndFeaturesSupport() { model.UseCaseActorTypeEVSE, model.UseCaseNameTypeEVSECommissioningAndConfiguration, []model.UseCaseScenarioSupportType{1}, - []model.FeatureTypeType{}, + nil, ) assert.Equal(s.T(), true, result) diff --git a/spine/entity_local.go b/spine/entity_local.go index 1b41ec5e..71d1ed58 100644 --- a/spine/entity_local.go +++ b/spine/entity_local.go @@ -84,14 +84,67 @@ func (r *EntityLocalImpl) Feature(addressFeature *model.AddressFeatureType) Feat } func (r *EntityLocalImpl) Information() *model.NodeManagementDetailedDiscoveryEntityInformationType { - res := model.NodeManagementDetailedDiscoveryEntityInformationType{ + res := &model.NodeManagementDetailedDiscoveryEntityInformationType{ Description: &model.NetworkManagementEntityDescriptionDataType{ EntityAddress: r.Address(), EntityType: &r.eType, }, } - return &res + return res +} + +// add a new usecase +func (r *EntityLocalImpl) AddUseCaseSupport( + actor model.UseCaseActorType, + useCaseName model.UseCaseNameType, + useCaseVersion model.SpecificationVersionType, + useCaseDocumemtSubRevision string, + useCaseAvailable bool, + scenarios []model.UseCaseScenarioSupportType, +) { + nodeMgmt := r.device.nodeManagement + + data := nodeMgmt.DataCopy(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) + + address := model.FeatureAddressType{ + Device: r.address.Device, + Entity: r.address.Entity, + } + + data.AddUseCaseSupport(address, actor, useCaseName, useCaseVersion, useCaseDocumemtSubRevision, useCaseAvailable, scenarios) + + nodeMgmt.SetData(model.FunctionTypeNodeManagementUseCaseData, data) +} + +// Remove a usecase with a given actor ans usecase name +func (r *EntityLocalImpl) RemoveUseCaseSupport( + actor model.UseCaseActorType, + useCaseName model.UseCaseNameType, +) { + nodeMgmt := r.device.nodeManagement + + data := nodeMgmt.DataCopy(model.FunctionTypeNodeManagementUseCaseData).(*model.NodeManagementUseCaseDataType) + + if data == nil { + return + } + + address := model.FeatureAddressType{ + Device: r.address.Device, + Entity: r.address.Entity, + } + + data.RemoveUseCaseSupport(address, actor, useCaseName) + + nodeMgmt.SetData(model.FunctionTypeNodeManagementUseCaseData, data) +} + +// Remove all usecases +func (r *EntityLocalImpl) RemoveAllUseCaseSupports() { + r.RemoveUseCaseSupport("", "") +} + // Remove all subscriptions func (r *EntityLocalImpl) RemoveAllSubscriptions() { for _, item := range r.features { diff --git a/spine/feature_local.go b/spine/feature_local.go index e9447f76..84669ddd 100644 --- a/spine/feature_local.go +++ b/spine/feature_local.go @@ -190,6 +190,9 @@ func (r *FeatureLocalImpl) RequestAndFetchData( // Subscribe to a remote feature func (r *FeatureLocalImpl) Subscribe(remoteAddress *model.FeatureAddressType) (*model.MsgCounterType, *model.ErrorType) { + if remoteAddress.Device == nil { + return nil, model.NewErrorTypeFromString("device not found") + } remoteDevice := r.entity.device.RemoteDeviceForAddress(*remoteAddress.Device) if remoteDevice == nil { return nil, model.NewErrorTypeFromString("device not found") diff --git a/spine/function_data_factory.go b/spine/function_data_factory.go index f2a99328..2e0b1202 100644 --- a/spine/function_data_factory.go +++ b/spine/function_data_factory.go @@ -7,10 +7,6 @@ import ( ) func CreateFunctionData[F any](featureType model.FeatureTypeType) []F { - if featureType == model.FeatureTypeTypeNodeManagement { - return []F{} // NodeManagement implementation is not using function data - } - // Some devices use generic for everything (e.g. Vaillant Arotherm heatpump) // or for some things like the SMA HM 2.0 or Elli Wallbox, which uses Generic feature // for Heartbeats, even though that should go into FeatureTypeTypeDeviceDiagnosis @@ -18,6 +14,16 @@ func CreateFunctionData[F any](featureType model.FeatureTypeType) []F { var result []F + if featureType == model.FeatureTypeTypeNodeManagement { + result = []F{ + createFunctionData[model.NodeManagementDestinationListDataType, F](model.FunctionTypeNodeManagementDestinationListData), + createFunctionData[model.NodeManagementDetailedDiscoveryDataType, F](model.FunctionTypeNodeManagementDetailedDiscoveryData), + createFunctionData[model.NodeManagementUseCaseDataType, F](model.FunctionTypeNodeManagementUseCaseData), + } + + return result + } + if featureType == model.FeatureTypeTypeActuatorLevel || featureType == model.FeatureTypeTypeGeneric { result = append(result, []F{ createFunctionData[model.ActuatorLevelDataType, F](model.FunctionTypeActuatorLevelData), diff --git a/spine/function_data_factory_test.go b/spine/function_data_factory_test.go index af63826d..d51e4b19 100644 --- a/spine/function_data_factory_test.go +++ b/spine/function_data_factory_test.go @@ -87,7 +87,7 @@ func TestFunctionDataFactory_FunctionDataCmd(t *testing.T) { func TestFunctionDataFactory_NodeMgmtFeatureType(t *testing.T) { result := CreateFunctionData[FunctionDataCmd](model.FeatureTypeTypeNodeManagement) - assert.Equal(t, 0, len(result)) + assert.Equal(t, 3, len(result)) } func TestFunctionDataFactory_unknownFunctionDataType(t *testing.T) { diff --git a/spine/model/nodemanagement_additions.go b/spine/model/nodemanagement_additions.go index a3466f67..1ad61bca 100644 --- a/spine/model/nodemanagement_additions.go +++ b/spine/model/nodemanagement_additions.go @@ -1,5 +1,12 @@ package model +import ( + "reflect" + "sync" +) + +var nmMux sync.Mutex + // NodeManagementDestinationListDataType var _ Updater = (*NodeManagementDestinationListDataType)(nil) @@ -12,3 +19,125 @@ func (r *NodeManagementDestinationListDataType) UpdateList(newList any, filterPa r.NodeManagementDestinationData = UpdateList(r.NodeManagementDestinationData, newData, filterPartial, filterDelete) } + +// NodeManagementUseCaseDataType + +// find the matching UseCaseInformation index for +// a given FeatureAddressType, UseCaseActorType and UseCaseNameType +// +// if UseCaseActorType and UseCaseNameType are empty they are ignored, +// and the first matching UseCaseInformation item is returned +func (n *NodeManagementUseCaseDataType) useCaseInformationIndex( + address FeatureAddressType, + actor UseCaseActorType, + useCaseName UseCaseNameType, +) (int, bool) { + + // get the element with the same entity + for index, item := range n.UseCaseInformation { + if item.Address.Device == nil || + item.Address.Entity == nil || + !reflect.DeepEqual(item.Address.Device, address.Device) || + !reflect.DeepEqual(item.Address.Entity, address.Entity) { + continue + } + + if len(actor) == 0 && len(useCaseName) == 0 { + return index, true + } + + if len(actor) > 0 { + if item.Actor == nil || *item.Actor != actor { + continue + } + + } + + if len(useCaseName) == 0 { + return index, true + } + + if _, ok := item.useCaseSupportIndex(useCaseName); ok { + return index, true + } + } + + return -1, false +} + +// add a new UseCaseSupportType +func (n *NodeManagementUseCaseDataType) AddUseCaseSupport( + address FeatureAddressType, + actor UseCaseActorType, + useCaseName UseCaseNameType, + useCaseVersion SpecificationVersionType, + useCaseDocumemtSubRevision string, + useCaseAvailable bool, + scenarios []UseCaseScenarioSupportType, +) { + nmMux.Lock() + defer nmMux.Unlock() + + useCaseSupport := UseCaseSupportType{ + UseCaseName: &useCaseName, + UseCaseVersion: &useCaseVersion, + UseCaseAvailable: &useCaseAvailable, + ScenarioSupport: scenarios, + UseCaseDocumentSubRevision: &useCaseDocumemtSubRevision, + } + + // is there an entry for the entity address and actor + usecaseIndex, ok := n.useCaseInformationIndex(address, actor, "") + + if ok { + n.UseCaseInformation[usecaseIndex].Add(useCaseSupport) + } else { + // create a new element for this entity + useCaseInformation := UseCaseInformationDataType{ + Address: &FeatureAddressType{ + Device: address.Device, + Entity: address.Entity, + }, + Actor: &actor, + UseCaseSupport: []UseCaseSupportType{useCaseSupport}, + } + n.UseCaseInformation = append(n.UseCaseInformation, useCaseInformation) + } +} + +// Remove a UseCaseSupportType with +// a provided FeatureAddressType, UseCaseActorType and UseCaseNameType +func (n *NodeManagementUseCaseDataType) RemoveUseCaseSupport( + address FeatureAddressType, + actor UseCaseActorType, + useCaseName UseCaseNameType, +) { + nmMux.Lock() + defer nmMux.Unlock() + + // is there an entry for the entity address, actor and usecase name + usecaseIndex, ok := n.useCaseInformationIndex(address, actor, useCaseName) + if !ok { + return + } + + var usecaseInfo []UseCaseInformationDataType + + for index, item := range n.UseCaseInformation { + if index != usecaseIndex { + usecaseInfo = append(usecaseInfo, item) + continue + } + + item.Remove(useCaseName) + + // only add the item if there are any usecases left + if len(item.UseCaseSupport) == 0 { + continue + } + + usecaseInfo = append(usecaseInfo, item) + } + + n.UseCaseInformation = usecaseInfo +} diff --git a/spine/model/usecaseinformation_additions.go b/spine/model/usecaseinformation_additions.go new file mode 100644 index 00000000..685247af --- /dev/null +++ b/spine/model/usecaseinformation_additions.go @@ -0,0 +1,54 @@ +package model + +import "sync" + +var uciMux sync.Mutex + +// UseCaseInformationDataType + +// find the matching UseCaseSupport index for a UseCaseNameType +func (u *UseCaseInformationDataType) useCaseSupportIndex(useCaseName UseCaseNameType) (int, bool) { + // get the element with the same entity + for index, item := range u.UseCaseSupport { + if item.UseCaseName != nil && *item.UseCaseName == useCaseName { + return index, true + } + } + + return -1, false +} + +// add a new UseCaseSupportType +func (u *UseCaseInformationDataType) Add(useCase UseCaseSupportType) { + uciMux.Lock() + defer uciMux.Unlock() + + if useCase.UseCaseName == nil { + return + } + + // only add it if it does not exist yet + if _, ok := u.useCaseSupportIndex(*useCase.UseCaseName); ok { + return + } + + u.UseCaseSupport = append(u.UseCaseSupport, useCase) +} + +// remove a UseCaseSupportType with a given UseCaseNameType +func (u *UseCaseInformationDataType) Remove(useCaseName UseCaseNameType) { + uciMux.Lock() + defer uciMux.Unlock() + + var usecases []UseCaseSupportType + + for _, item := range u.UseCaseSupport { + if item.UseCaseName != nil && *item.UseCaseName != useCaseName { + continue + } + + usecases = append(usecases, item) + } + + u.UseCaseSupport = usecases +} diff --git a/spine/nodemanagement_usecase.go b/spine/nodemanagement_usecase.go index f21c6e63..626c6641 100644 --- a/spine/nodemanagement_usecase.go +++ b/spine/nodemanagement_usecase.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" - "github.com/enbility/eebus-go/logging" "github.com/enbility/eebus-go/spine/model" "github.com/enbility/eebus-go/util" ) @@ -23,86 +22,20 @@ func (r *NodeManagementImpl) NotifyUseCaseData(remoteDevice *DeviceRemoteImpl) ( featureRemote := remoteDevice.FeatureByEntityTypeAndRole(rEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) - cmd := model.CmdType{ - NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ - UseCaseInformation: r.entity.Device().UseCaseManager().UseCaseInformation(), - }, - } + fd := r.functionData(model.FunctionTypeNodeManagementUseCaseData) + cmd := fd.NotifyCmdType(nil, nil, false, nil) return featureRemote.Sender().Notify(r.Address(), rfAdress, cmd) } func (r *NodeManagementImpl) processReadUseCaseData(featureRemote *FeatureRemoteImpl, requestHeader *model.HeaderType) error { - - cmd := model.CmdType{ - NodeManagementUseCaseData: &model.NodeManagementUseCaseDataType{ - UseCaseInformation: r.entity.Device().UseCaseManager().UseCaseInformation(), - }, - } + cmd := r.functionData(model.FunctionTypeNodeManagementUseCaseData).ReplyCmdType(false) return featureRemote.Sender().Reply(requestHeader, r.Address(), cmd) } -func (r *NodeManagementImpl) processReplyUseCaseData(message *Message, data model.NodeManagementUseCaseDataType) error { - useCaseInformation := data.UseCaseInformation - if useCaseInformation == nil { - return errors.New("nodemanagement.replyUseCaseData: invalid UseCaseInformation") - } - - remoteUseCaseManager := message.FeatureRemote.Device().UseCaseManager() - remoteUseCaseManager.RemoveAll() - - for _, useCaseInfo := range useCaseInformation { - // this is mandatory - var actor model.UseCaseActorType - if useCaseInfo.Actor != nil { - actor = model.UseCaseActorType(*useCaseInfo.Actor) - } else { - logging.Log().Debug("actor is missing in useCaseInformation") - break - } - - for _, useCaseSupport := range useCaseInfo.UseCaseSupport { - - // this is mandatory - var useCaseName model.UseCaseNameType - if useCaseSupport.UseCaseName != nil { - useCaseName = model.UseCaseNameType(*useCaseSupport.UseCaseName) - } else { - logging.Log().Debug("useCaseName is missing in useCaseSupport") - continue - } - - // this is optional - useCaseAvailable := true - if useCaseSupport.UseCaseAvailable != nil { - useCaseAvailable = *useCaseSupport.UseCaseAvailable - } - - var useCaseVersion model.SpecificationVersionType - if useCaseSupport.UseCaseVersion != nil { - useCaseVersion = model.SpecificationVersionType(*useCaseSupport.UseCaseVersion) - } - - var useCaseDocumemtSubRevision string - if useCaseSupport.UseCaseDocumentSubRevision != nil { - useCaseDocumemtSubRevision = *useCaseSupport.UseCaseDocumentSubRevision - } - - if useCaseSupport.ScenarioSupport == nil { - logging.Log().Errorf("scenarioSupport is missing in useCaseSupport %s", useCaseName) - continue - } - - remoteUseCaseManager.Add( - actor, - useCaseName, - useCaseVersion, - useCaseDocumemtSubRevision, - useCaseAvailable, - useCaseSupport.ScenarioSupport) - } - } +func (r *NodeManagementImpl) processReplyUseCaseData(message *Message, data *model.NodeManagementUseCaseDataType) error { + message.FeatureRemote.UpdateData(model.FunctionTypeNodeManagementUseCaseData, data, nil, nil) // the data was updated, so send an event, other event handlers may watch out for this as well payload := EventPayload{ @@ -129,10 +62,10 @@ func (r *NodeManagementImpl) handleMsgUseCaseData(message *Message, data *model. if err := r.pendingRequests.Remove(message.DeviceRemote.ski, *message.RequestHeader.MsgCounterReference); err != nil { return errors.New(err.String()) } - return r.processReplyUseCaseData(message, *data) + return r.processReplyUseCaseData(message, data) case model.CmdClassifierTypeNotify: - return r.processReplyUseCaseData(message, *data) + return r.processReplyUseCaseData(message, data) default: return fmt.Errorf("nodemanagement.handleUseCaseData: NodeManagementUseCaseData CmdClassifierType not implemented: %s", message.CmdClassifier) diff --git a/spine/testdata/nm_usecaseinformationlistdata_recv_reply.json b/spine/testdata/nm_usecaseinformationlistdata_recv_reply.json new file mode 100644 index 00000000..14d86965 --- /dev/null +++ b/spine/testdata/nm_usecaseinformationlistdata_recv_reply.json @@ -0,0 +1,120 @@ +{ + "datagram": { + "header": { + "specificationVersion": "1.3.0", + "addressSource": { + "device": "TestDeviceAddress", + "entity": [ + 0 + ], + "feature": 0 + }, + "addressDestination": { + "device": "HEMS", + "entity": [ + 0 + ], + "feature": 0 + }, + "msgCounter": 2, + "msgCounterReference": 1, + "cmdClassifier": "reply" + }, + "payload": { + "cmd": [ + { + "nodeManagementUseCaseData": { + "useCaseInformation": [ + { + "address": { + "device": "d:_i:47859_Elli-Wallbox-xxxxxxxxxx" + }, + "actor": "EVSE", + "useCaseSupport": [ + { + "useCaseName": "evseCommissioningAndConfiguration", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1, + 2 + ] + }, + { + "useCaseName": "evChargingSummary", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1 + ] + } + ] + }, + { + "address": { + "device": "d:_i:47859_Elli-Wallbox-2041A0ZW5R" + }, + "actor": "EV", + "useCaseSupport": [ + { + "useCaseName": "evCommissioningAndConfiguration", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + { + "useCaseName": "overloadProtectionByEvChargingCurrentCurtailment", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1, + 2, + 3 + ] + }, + { + "useCaseName": "coordinatedEvCharging", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ] + }, + { + "useCaseName": "measurementOfElectricityDuringEvCharging", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1, + 2, + 3 + ] + }, + { + "useCaseName": "optimizationOfSelfConsumptionDuringEvCharging", + "useCaseVersion": "1.0.1", + "scenarioSupport": [ + 1, + 2, + 3 + ] + } + ] + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/spine/usecase.go b/spine/usecase.go deleted file mode 100644 index b28be496..00000000 --- a/spine/usecase.go +++ /dev/null @@ -1,157 +0,0 @@ -package spine - -import ( - "fmt" - - "github.com/ahmetb/go-linq/v3" - "github.com/enbility/eebus-go/spine/model" -) - -// a default mapping of a given EntityTypeType to a UseCaseActorType -var entityTypeActorMap = map[model.EntityTypeType]model.UseCaseActorType{ - model.EntityTypeTypeBattery: model.UseCaseActorTypeBattery, - model.EntityTypeTypeCEM: model.UseCaseActorTypeCEM, - model.EntityTypeTypeCompressor: model.UseCaseActorTypeCompressor, - model.EntityTypeTypeElectricityStorageSystem: model.UseCaseActorTypeBatterySystem, - model.EntityTypeTypeElectricityGenerationSystem: model.UseCaseActorTypePVSystem, - model.EntityTypeTypeEV: model.UseCaseActorTypeEV, - model.EntityTypeTypeEVSE: model.UseCaseActorTypeEVSE, - model.EntityTypeTypeDHWCircuit: model.UseCaseActorTypeDHWCircuit, - model.EntityTypeTypeHeatingCircuit: model.UseCaseActorTypeHeatingCircuit, - model.EntityTypeTypeHeatPumpAppliance: model.UseCaseActorTypeHeatPump, - model.EntityTypeTypeHvacRoom: model.UseCaseActorTypeHVACRoom, - model.EntityTypeTypeInverter: model.UseCaseActorTypeInverter, - model.EntityTypeTypeSmartEnergyAppliance: model.UseCaseActorTypeControllableSystem, - model.EntityTypeTypeSubMeterElectricity: model.UseCaseActorTypeControllableSystem, - model.EntityTypeTypeGridConnectionPointOfPremises: model.UseCaseActorTypeGridConnectionPoint, -} - -// list of known use cases and the allowed actors for each -var useCaseValidActorsMap = map[model.UseCaseNameType][]model.UseCaseActorType{ - model.UseCaseNameTypeConfigurationOfDhwSystemFunction: {model.UseCaseActorTypeConfigurationAppliance, model.UseCaseActorTypeDHWCircuit}, - model.UseCaseNameTypeConfigurationOfDhwTemperature: {model.UseCaseActorTypeConfigurationAppliance, model.UseCaseActorTypeDHWCircuit}, - model.UseCaseNameTypeConfigurationOfRoomCoolingSystemFunction: {model.UseCaseActorTypeConfigurationAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeConfigurationOfRoomCoolingTemperature: {model.UseCaseActorTypeConfigurationAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeConfigurationOfRoomHeatingSystemFunction: {model.UseCaseActorTypeConfigurationAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeConfigurationOfRoomHeatingTemperature: {model.UseCaseActorTypeConfigurationAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeControlOfBattery: {model.UseCaseActorTypeInverter, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeCoordinatedEVCharging: {model.UseCaseActorTypeEV, model.UseCaseActorTypeCEM, model.UseCaseActorTypeEnergyBroker}, - model.UseCaseNameTypeEVChargingSummary: {model.UseCaseActorTypeEVSE, model.UseCaseActorTypeCEM, model.UseCaseActorTypeEnergyBroker}, - model.UseCaseNameTypeEVCommissioningAndConfiguration: {model.UseCaseActorTypeEV, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeEVSECommissioningAndConfiguration: {model.UseCaseActorTypeEVSE, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeEVStateOfCharge: {model.UseCaseActorTypeEV, model.UseCaseActorTypeMonitoringAppliance}, - model.UseCaseNameTypeFlexibleLoad: {model.UseCaseActorTypeEnergyConsumer, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeFlexibleStartForWhiteGoods: {model.UseCaseActorTypeSmartAppliance, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeIncentiveTableBasedPowerConsumptionManagement: {model.UseCaseActorTypeEnergyConsumer, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeLimitationOfPowerConsumption: {model.UseCaseActorTypeEnergyGuard, model.UseCaseActorTypeControllableSystem}, - model.UseCaseNameTypeLimitationOfPowerProduction: {model.UseCaseActorTypeEnergyGuard, model.UseCaseActorTypeControllableSystem}, - model.UseCaseNameTypeMeasurementOfElectricityDuringEVCharging: {model.UseCaseActorTypeEV, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeMonitoringAndControlOfSmartGridReadyConditions: {model.UseCaseActorTypeHeatPump, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeMonitoringOfBattery: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeBattery}, - model.UseCaseNameTypeMonitoringOfDhwSystemFunction: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeDHWCircuit}, - model.UseCaseNameTypeMonitoringOfDhwTemperature: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeDHWCircuit}, - model.UseCaseNameTypeMonitoringOfGridConnectionPoint: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeGridConnectionPoint}, - model.UseCaseNameTypeMonitoringOfInverter: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeInverter}, - model.UseCaseNameTypeMonitoringOfOutdoorTemperature: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeOutdoorTemperatureSensor}, - model.UseCaseNameTypeMonitoringOfPowerConsumption: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeMonitoredUnit}, - model.UseCaseNameTypeMonitoringOfPvString: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypePVString}, - model.UseCaseNameTypeMonitoringOfRoomCoolingSystemFunction: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeMonitoringOfRoomHeatingSystemFunction: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeMonitoringOfRoomTemperature: {model.UseCaseActorTypeMonitoringAppliance, model.UseCaseActorTypeHVACRoom}, - model.UseCaseNameTypeOptimizationOfSelfConsumptionByHeatPumpCompressorFlexibility: {model.UseCaseActorTypeCompressor, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging: {model.UseCaseActorTypeEV, model.UseCaseActorTypeCEM}, - model.UseCaseNameTypeOverloadProtectionByEVChargingCurrentCurtailment: {model.UseCaseActorTypeEV, model.UseCaseActorTypeCEM, model.UseCaseActorTypeEnergyGuard}, - model.UseCaseNameTypeVisualizationOfAggregatedBatteryData: {model.UseCaseActorTypeVisualizationAppliance, model.UseCaseActorTypeBatterySystem}, - model.UseCaseNameTypeVisualizationOfAggregatedPhotovoltaicData: {model.UseCaseActorTypeVisualizationAppliance, model.UseCaseActorTypePVSystem}, - model.UseCaseNameTypeVisualizationOfHeatingAreaName: {model.UseCaseActorTypeVisualizationAppliance, model.UseCaseActorTypeHeatingCircuit, model.UseCaseActorTypeHeatingZone, model.UseCaseActorTypeHVACRoom}, -} - -// defines a specific usecase implementation -// right now this is just used as a wrapper for supported usecases -type UseCaseImpl struct { - Entity *EntityLocalImpl - Actor model.UseCaseActorType - - name model.UseCaseNameType - useCaseVersion model.SpecificationVersionType - useCaseAvailable bool - scenarioSupport []model.UseCaseScenarioSupportType -} - -// returns a UseCaseImpl with a default mapping of entity to actor using data -func NewUseCase( - entity *EntityLocalImpl, - ucEnumType model.UseCaseNameType, - useCaseVersion model.SpecificationVersionType, - useCaseDocumemtSubRevision string, - useCaseAvailable bool, - scenarioSupport []model.UseCaseScenarioSupportType, -) *UseCaseImpl { - checkEntityArguments(*entity.EntityImpl) - - actor := entityTypeActorMap[entity.EntityType()] - - return NewUseCaseWithActor(entity, actor, ucEnumType, useCaseVersion, useCaseDocumemtSubRevision, useCaseAvailable, scenarioSupport) -} - -// returns a UseCaseImpl with specific entity and actor -func NewUseCaseWithActor( - entity *EntityLocalImpl, - actor model.UseCaseActorType, - ucEnumType model.UseCaseNameType, - useCaseVersion model.SpecificationVersionType, - useCaseDocumemtSubRevision string, - useCaseAvailable bool, - scenarioSupport []model.UseCaseScenarioSupportType, -) *UseCaseImpl { - checkUCArguments(actor, ucEnumType) - - ucManager := entity.Device().UseCaseManager() - ucManager.Add(actor, ucEnumType, useCaseVersion, useCaseDocumemtSubRevision, useCaseAvailable, scenarioSupport) - - return &UseCaseImpl{ - Entity: entity, - Actor: actor, - name: model.UseCaseNameType(ucEnumType), - useCaseVersion: useCaseVersion, - useCaseAvailable: useCaseAvailable, - scenarioSupport: scenarioSupport, - } -} - -// check if there is an predefined mapping available -func checkEntityArguments(entity EntityImpl) { - actor := entityTypeActorMap[entity.EntityType()] - if actor == "" { - panic(fmt.Errorf("cannot derive actor for entity type '%s'", entity.EntityType())) - } -} - -// check if the actor is valid for the given usecase type -func checkUCArguments(actor model.UseCaseActorType, ucEnumType model.UseCaseNameType) { - if !linq.From(useCaseValidActorsMap[ucEnumType]).Contains(actor) { - panic(fmt.Errorf("the actor '%s' is not valid for the use case '%s'", actor, ucEnumType)) - } -} - -// Update the availability of this usecase and -// trigger a notification being sent to the remote device -func (u *UseCaseImpl) SetUseCaseAvailable(available bool) { - u.useCaseAvailable = available - - u.Entity.Device().NotifyUseCaseData() -} - -/* -// This is not yet used, might be removed? -func waitForRequest[T any](c chan T, maxDelay time.Duration) *T { - timeout := time.After(maxDelay) - - select { - case data := <-c: - return &data - case <-timeout: - return nil - } -} -*/ diff --git a/spine/usecase_manager.go b/spine/usecase_manager.go deleted file mode 100644 index d4d24e29..00000000 --- a/spine/usecase_manager.go +++ /dev/null @@ -1,78 +0,0 @@ -package spine - -import ( - "github.com/enbility/eebus-go/spine/model" -) - -// manages the supported usecases for a device -// each device has its own UseCaseManager -type UseCaseManager struct { - useCaseInformationMap map[model.UseCaseActorType][]model.UseCaseSupportType - - localDevice *DeviceImpl -} - -// return a new UseCaseManager -func NewUseCaseManager(localDevice *DeviceImpl) *UseCaseManager { - return &UseCaseManager{ - useCaseInformationMap: make(map[model.UseCaseActorType][]model.UseCaseSupportType), - localDevice: localDevice, - } -} - -// add a usecase -func (r *UseCaseManager) Add( - actor model.UseCaseActorType, - useCaseName model.UseCaseNameType, - useCaseVersion model.SpecificationVersionType, - useCaseDocumemtSubRevision string, - useCaseAvailable bool, - scenarios []model.UseCaseScenarioSupportType, -) { - useCaseSupport := model.UseCaseSupportType{ - UseCaseVersion: &useCaseVersion, - UseCaseName: &useCaseName, - UseCaseAvailable: &useCaseAvailable, - ScenarioSupport: scenarios, - } - - if len(useCaseDocumemtSubRevision) > 0 { - useCaseSupport.UseCaseDocumentSubRevision = &useCaseDocumemtSubRevision - } - - useCaseInfo, exists := r.useCaseInformationMap[actor] - if !exists { - useCaseInfo = make([]model.UseCaseSupportType, 0) - } - useCaseInfo = append(useCaseInfo, useCaseSupport) - - r.useCaseInformationMap[actor] = useCaseInfo -} - -// this needs to be called when a new notification or reply will provide a new set of UseCases -func (r *UseCaseManager) RemoveAll() { - r.useCaseInformationMap = make(map[model.UseCaseActorType][]model.UseCaseSupportType) -} - -// return all actors and their supported usecases -func (r *UseCaseManager) UseCaseInformation() []model.UseCaseInformationDataType { - var result []model.UseCaseInformationDataType - - for actor, useCaseSupport := range r.useCaseInformationMap { - thisActor := actor - // according to ProtocolSpecification Version 1.3.0 chapter 7.5.2 - // the address is mandatory. At least the device address should be shown, - // preferably the entity and features as well - deviceAddress := &model.FeatureAddressType{ - Device: r.localDevice.Address(), - } - useCaseInfo := model.UseCaseInformationDataType{ - Address: deviceAddress, - Actor: &thisActor, - UseCaseSupport: useCaseSupport, - } - result = append(result, useCaseInfo) - } - - return result -} diff --git a/spine/usecase_test.go b/spine/usecase_test.go deleted file mode 100644 index 415795e2..00000000 --- a/spine/usecase_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package spine - -import ( - "testing" - "time" - - "github.com/enbility/eebus-go/spine/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -func TestUsecaseSuite(t *testing.T) { - suite.Run(t, new(UsecaseSuite)) -} - -type UsecaseSuite struct { - suite.Suite - - device *DeviceLocalImpl - entity *EntityLocalImpl -} - -func (s *UsecaseSuite) BeforeTest(suiteName, testName string) { - s.device = NewDeviceLocalImpl("brand", "model", "serial", "code", "address", model.DeviceTypeTypeEnergyManagementSystem, model.NetworkManagementFeatureSetTypeSmart, time.Second*4) - s.entity = NewEntityLocalImpl(s.device, model.EntityTypeTypeCEM, NewAddressEntityType([]uint{1})) - s.device.AddEntity(s.entity) - -} - -func (s *UsecaseSuite) Test_UseCase() { - uc := NewUseCase( - s.entity, - model.UseCaseNameTypeControlOfBattery, - model.SpecificationVersionType("1.0.0"), - "", - true, - []model.UseCaseScenarioSupportType{1}, - ) - assert.NotNil(s.T(), uc) - - uc.SetUseCaseAvailable(true) -}