diff --git a/usecases/README.md b/usecases/README.md index 47a977b1..58bc89ca 100644 --- a/usecases/README.md +++ b/usecases/README.md @@ -34,3 +34,4 @@ Actors: Use Cases: - `mpc`: Monitoring of Power Consumption - `mgcp`: Monitoring of Grid Connection Point + - `mdt`: Monitoring of DHW Temperature diff --git a/usecases/api/ma_mdt.go b/usecases/api/ma_mdt.go new file mode 100644 index 00000000..becefdfa --- /dev/null +++ b/usecases/api/ma_mdt.go @@ -0,0 +1,24 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Monitoring Appliance +// UseCase: Monitoring of DHW Temperature +type MaMDTInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the momentary temperature of the domestic hot water circuit + // + // parameters: + // - entity: the entity of the device (e.g. DHWCircuit) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + Temperature(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/ma/mdt/events.go b/usecases/ma/mdt/events.go new file mode 100644 index 00000000..c2bfe627 --- /dev/null +++ b/usecases/ma/mdt/events.go @@ -0,0 +1,80 @@ +package mdt + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *MDT) HandleEvent(payload spineapi.EventPayload) { + // only about events from a DHWCircuit entity or device changes for this remote device + + if !e.IsCompatibleEntityType(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.deviceConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.MeasurementDescriptionListDataType: + e.deviceMeasurementDescriptionDataUpdate(payload.Entity) + + case *model.MeasurementListDataType: + e.deviceMeasurementDataUpdate(payload) + } +} + +// process required steps when a device is connected +func (e *MDT) deviceConnected(entity spineapi.EntityRemoteInterface) { + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if !measurement.HasSubscription() { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(nil, nil); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(nil, nil); err != nil { + logging.Log().Error(err) + } + } +} + +// the measurement descriptiondata of a device was updated +func (e *MDT) deviceMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestData(nil, nil); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of a device was updated +func (e *MDT) deviceMeasurementDataUpdate(payload spineapi.EventPayload) { + if measurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + // Scenario 1 + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + } + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) && e.EventCB != nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateDhwTemperature) + } + } +} diff --git a/usecases/ma/mdt/events_test.go b/usecases/ma/mdt/events_test.go new file mode 100644 index 00000000..c79cd2a0 --- /dev/null +++ b/usecases/ma/mdt/events_test.go @@ -0,0 +1,86 @@ +package mdt + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MaMDTSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = util.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.NodeManagementUseCaseDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *MaMDTSuite) Test_Failures() { + s.sut.deviceConnected(s.mockRemoteEntity) + + s.sut.deviceMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *MaMDTSuite) Test_deviceMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.deviceMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.deviceMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.deviceMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/ma/mdt/public.go b/usecases/ma/mdt/public.go new file mode 100644 index 00000000..968c50d9 --- /dev/null +++ b/usecases/ma/mdt/public.go @@ -0,0 +1,42 @@ +package mdt + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// Scenario 1 + +// return the momentary temperature of the domestic hot water circuit +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *MDT) Temperature(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntityType(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeTemperature), + CommodityType: util.Ptr(model.CommodityTypeTypeDomestichotwater), + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + } + data, err := measurement.GetDataForFilter(filter) + if err != nil || len(data) == 0 || data[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + + // take the first item + value := data[0].Value + + return value.GetValue(), nil +} diff --git a/usecases/ma/mdt/public_test.go b/usecases/ma/mdt/public_test.go new file mode 100644 index 00000000..92be1f34 --- /dev/null +++ b/usecases/ma/mdt/public_test.go @@ -0,0 +1,52 @@ +package mdt + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MaMDTSuite) Test_Temperature() { + data, err := s.sut.Temperature(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Temperature(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeTemperature), + CommodityType: util.Ptr(model.CommodityTypeTypeDomestichotwater), + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Temperature(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(55), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Temperature(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 55.0, data) +} diff --git a/usecases/ma/mdt/testhelper_test.go b/usecases/ma/mdt/testhelper_test.go new file mode 100644 index 00000000..84515cce --- /dev/null +++ b/usecases/ma/mdt/testhelper_test.go @@ -0,0 +1,172 @@ +package mdt + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestMaMDTSuite(t *testing.T) { + suite.Run(t, new(MaMDTSuite)) +} + +type MaMDTSuite struct { + suite.Suite + + sut *MDT + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *MaMDTSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *MaMDTSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewMDT(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeDHWCircuit), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + for _, entity := range entities { + entity.UpdateDeviceAddress(*remoteDevice.Address()) + } + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/usecases/ma/mdt/types.go b/usecases/ma/mdt/types.go new file mode 100644 index 00000000..a88feb86 --- /dev/null +++ b/usecases/ma/mdt/types.go @@ -0,0 +1,17 @@ +package mdt + +import "github.com/enbility/eebus-go/api" + +const ( + // Update of the list of remote entities supporting the Use Case + // + // Use `RemoteEntities` to get the current data + UseCaseSupportUpdate api.EventType = "ma-mdt-UseCaseSupportUpdate" + + // DHW Temperature + // + // Use `Temperature` to get the current data + // + // Use Case MDT, Scenario 1 + DataUpdateDhwTemperature api.EventType = "ma-mdt-DataUpdateDhwTemperature" +) diff --git a/usecases/ma/mdt/usecase.go b/usecases/ma/mdt/usecase.go new file mode 100644 index 00000000..362d59cb --- /dev/null +++ b/usecases/ma/mdt/usecase.go @@ -0,0 +1,62 @@ +package mdt + +import ( + "github.com/enbility/eebus-go/api" + ucapi "github.com/enbility/eebus-go/usecases/api" + usecase "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type MDT struct { + *usecase.UseCaseBase +} + +var _ ucapi.MaMDTInterface = (*MDT)(nil) + +func NewMDT(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *MDT { + validActorTypes := []model.UseCaseActorType{model.UseCaseActorTypeMonitoredUnit} + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeDHWCircuit, + } + useCaseScenarios := []api.UseCaseScenario{ + { + Scenario: model.UseCaseScenarioSupportType(1), + Mandatory: true, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeMeasurement, + }, + }, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeMonitoringAppliance, + model.UseCaseNameTypeMonitoringOfDhwTemperature, + "1.0.0", + "release", + useCaseScenarios, + eventCB, + UseCaseSupportUpdate, + validActorTypes, + validEntityTypes) + + uc := &MDT{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *MDT) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} diff --git a/usecases/ma/mdt/usecase_test.go b/usecases/ma/mdt/usecase_test.go new file mode 100644 index 00000000..d59fff5b --- /dev/null +++ b/usecases/ma/mdt/usecase_test.go @@ -0,0 +1,5 @@ +package mdt + +func (s *MaMDTSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +}