From 7dcecb4a1c65bc4a6354967c1dcb6e9e7bf662e0 Mon Sep 17 00:00:00 2001 From: Garry Blair Date: Mon, 14 Oct 2024 15:42:01 +0300 Subject: [PATCH] Feat: Add OHPCF read and write --- usecases/README.md | 1 + usecases/api/cem_ohpcf.go | 14 +++ usecases/cem/ohpcf/events.go | 32 +++++ usecases/cem/ohpcf/public.go | 82 ++++++++++++ usecases/cem/ohpcf/public_test.go | 48 +++++++ usecases/cem/ohpcf/testhelper_test.go | 172 ++++++++++++++++++++++++++ usecases/cem/ohpcf/types.go | 10 ++ usecases/cem/ohpcf/usecase.go | 74 +++++++++++ usecases/cem/ohpcf/usecase_test.go | 5 + 9 files changed, 438 insertions(+) create mode 100644 usecases/api/cem_ohpcf.go create mode 100644 usecases/cem/ohpcf/events.go create mode 100644 usecases/cem/ohpcf/public.go create mode 100644 usecases/cem/ohpcf/public_test.go create mode 100644 usecases/cem/ohpcf/testhelper_test.go create mode 100644 usecases/cem/ohpcf/types.go create mode 100644 usecases/cem/ohpcf/usecase.go create mode 100644 usecases/cem/ohpcf/usecase_test.go diff --git a/usecases/README.md b/usecases/README.md index 47a977b1..eab1966b 100644 --- a/usecases/README.md +++ b/usecases/README.md @@ -16,6 +16,7 @@ Actors: - `oscev`: Optimization of Self-Consumption During EV Charging - `vabd`: Visualization of Aggregated Battery Data - `vapd`: Visualization of Aggregated Photovoltaic Data + - `ohpcf`: Optiziation of Heatpump Compressor Flexibility - `cs`: Controllable System diff --git a/usecases/api/cem_ohpcf.go b/usecases/api/cem_ohpcf.go new file mode 100644 index 00000000..3c0e858a --- /dev/null +++ b/usecases/api/cem_ohpcf.go @@ -0,0 +1,14 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type CemOHPCFInterface interface { + api.UseCaseInterface + + SmartEnergyManagementData(entity spineapi.EntityRemoteInterface) ( + smartEnergyManagementData model.SmartEnergyManagementPsDataType, resultErr error) +} diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go new file mode 100644 index 00000000..de704087 --- /dev/null +++ b/usecases/cem/ohpcf/events.go @@ -0,0 +1,32 @@ +package ohpcf + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" +) + +// handle SPINE events +func (e *OHPCF) HandleEvent(payload spineapi.EventPayload) { + // only about events from a compressor entity or device changes for this remote device + + if !e.IsCompatibleEntityType(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + // get the smart energy management data from the remote entity + e.connected(payload.Entity) + } +} + +func (e *OHPCF) connected(entity spineapi.EntityRemoteInterface) { + smartEnergyManagement, err := client.NewSmartEnergyManagementPs(e.LocalEntity, entity) + if err != nil || smartEnergyManagement == nil { + return + } + if _, err := smartEnergyManagement.RequestData(); err != nil { + logging.Log().Debug(err) + } +} diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go new file mode 100644 index 00000000..7f33b5d2 --- /dev/null +++ b/usecases/cem/ohpcf/public.go @@ -0,0 +1,82 @@ +package ohpcf + +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 - Monitor heat pump compressor's power consumption flexibility + +// Read the current Smart Energy Management Data +// +// parameters: +// - entity: the entity of the e.g. HVAC +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *OHPCF) SmartEnergyManagementData(entity spineapi.EntityRemoteInterface) ( + smartEnergyManagementData model.SmartEnergyManagementPsDataType, resultErr error) { + + smartEnergyManagementData = model.SmartEnergyManagementPsDataType{ + NodeScheduleInformation: &model.PowerSequenceNodeScheduleInformationDataType{ + NodeRemoteControllable: util.Ptr(false), + SupportsSingleSlotSchedulingOnly: util.Ptr(false), + AlternativesCount: util.Ptr(uint(0)), + TotalSequencesCountMax: util.Ptr(uint(0)), + SupportsReselection: util.Ptr(false), + }, + } + resultErr = api.ErrNoCompatibleEntity + if !e.IsCompatibleEntityType(entity) { + return + } + + resultErr = api.ErrDataNotAvailable + smartEnergyManagement, err := client.NewSmartEnergyManagementPs(e.LocalEntity, entity) + if err != nil || smartEnergyManagement == nil { + return + } + + smartEnergyManagementDataPtr, err := smartEnergyManagement.GetData() + if err != nil || smartEnergyManagementDataPtr == nil { + return + } + smartEnergyManagementData = *smartEnergyManagementDataPtr + resultErr = nil + + return smartEnergyManagementData, resultErr +} + +// Scenario 2 - Control heat pump compressor's power consumption flexibility + +// Write the Smart Energy Management Data +// +// parameters: +// - entity: the entity of the heatpump compressor +// - value: the new limit in W +func (e *OHPCF) WriteSmartEnergyManagementData(entity spineapi.EntityRemoteInterface, + data *model.SmartEnergyManagementPsDataType) (*model.MsgCounterType, error) { + + if !e.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagement, err := client.NewSmartEnergyManagementPs(e.LocalEntity, entity) + if err != nil || smartEnergyManagement == nil { + return nil, api.ErrDataNotAvailable + } + + msgCounter, err := smartEnergyManagement.WriteData(data) + if err != nil { + return nil, err + } + + return msgCounter, nil +} diff --git a/usecases/cem/ohpcf/public_test.go b/usecases/cem/ohpcf/public_test.go new file mode 100644 index 00000000..91db3ac0 --- /dev/null +++ b/usecases/cem/ohpcf/public_test.go @@ -0,0 +1,48 @@ +package ohpcf + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CemOHPCFSuite) Test_NodeScheduleInformation() { + data, err := s.sut.SmartEnergyManagementData(nil) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), uint(0), *data.NodeScheduleInformation.AlternativesCount) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.NodeRemoteControllable) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.SupportsReselection) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.SupportsSingleSlotSchedulingOnly) + assert.Equal(s.T(), uint(0), *data.NodeScheduleInformation.TotalSequencesCountMax) + + data, err = s.sut.SmartEnergyManagementData(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), uint(0), *data.NodeScheduleInformation.AlternativesCount) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.NodeRemoteControllable) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.SupportsReselection) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.SupportsSingleSlotSchedulingOnly) + assert.Equal(s.T(), uint(0), *data.NodeScheduleInformation.TotalSequencesCountMax) + + smartEnergyManagementData := &model.SmartEnergyManagementPsDataType{ + NodeScheduleInformation: &model.PowerSequenceNodeScheduleInformationDataType{ + + NodeRemoteControllable: util.Ptr(true), + SupportsSingleSlotSchedulingOnly: util.Ptr(true), + AlternativesCount: util.Ptr(uint(1)), + TotalSequencesCountMax: util.Ptr(uint(3)), + SupportsReselection: util.Ptr(false), + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, smartEnergyManagementData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.SmartEnergyManagementData(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), uint(1), *data.NodeScheduleInformation.AlternativesCount) + assert.Equal(s.T(), true, *data.NodeScheduleInformation.NodeRemoteControllable) + assert.Equal(s.T(), false, *data.NodeScheduleInformation.SupportsReselection) + assert.Equal(s.T(), true, *data.NodeScheduleInformation.SupportsSingleSlotSchedulingOnly) + assert.Equal(s.T(), uint(3), *data.NodeScheduleInformation.TotalSequencesCountMax) +} diff --git a/usecases/cem/ohpcf/testhelper_test.go b/usecases/cem/ohpcf/testhelper_test.go new file mode 100644 index 00000000..633eed28 --- /dev/null +++ b/usecases/cem/ohpcf/testhelper_test.go @@ -0,0 +1,172 @@ +package ohpcf + +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 TestCemOHPCFSuite(t *testing.T) { + suite.Run(t, new(CemOHPCFSuite)) +} + +type CemOHPCFSuite struct { + suite.Suite + + sut *OHPCF + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *CemOHPCFSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *CemOHPCFSuite) 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 = NewOHPCF(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 + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeSmartEnergyManagementPs, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeSmartEnergyManagementPsData, + }, + }, + } + 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(feature.role), + 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.EntityTypeTypeCompressor), + }, + }, + }, + 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/cem/ohpcf/types.go b/usecases/cem/ohpcf/types.go new file mode 100644 index 00000000..58dcbdbd --- /dev/null +++ b/usecases/cem/ohpcf/types.go @@ -0,0 +1,10 @@ +package ohpcf + +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 = "cem-ohpcf-UseCaseSupportUpdate" +) diff --git a/usecases/cem/ohpcf/usecase.go b/usecases/cem/ohpcf/usecase.go new file mode 100644 index 00000000..9ba28151 --- /dev/null +++ b/usecases/cem/ohpcf/usecase.go @@ -0,0 +1,74 @@ +package ohpcf + +import ( + "github.com/enbility/eebus-go/api" + ucapi "github.com/enbility/eebus-go/usecases/api" + "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" +) + +// Optimization of Heat Pump Compressor Function +type OHPCF struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemOHPCFInterface = (*OHPCF)(nil) + +func NewOHPCF( + localEntity spineapi.EntityLocalInterface, + eventCB api.EntityEventCallback, +) *OHPCF { + validActorTypes := []model.UseCaseActorType{ + model.UseCaseActorTypeCEM, + model.UseCaseActorTypeCompressor, + } + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeCEM, + model.EntityTypeTypeCompressor, + } + useCaseScenarios := []api.UseCaseScenario{ + { + Scenario: model.UseCaseScenarioSupportType(1), + Mandatory: true, + ServerFeatures: []model.FeatureTypeType{model.FeatureTypeTypeSmartEnergyManagementPs}, + }, + { + Scenario: model.UseCaseScenarioSupportType(2), + Mandatory: true, + ServerFeatures: []model.FeatureTypeType{model.FeatureTypeTypeSmartEnergyManagementPs}, + }, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeOptimizationOfSelfConsumptionByHeatPumpCompressorFlexibility, + "1.0.0", + "release", + useCaseScenarios, + eventCB, + UseCaseSupportUpdate, + validActorTypes, + validEntityTypes, + ) + + uc := &OHPCF{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *OHPCF) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeSmartEnergyManagementPs, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} diff --git a/usecases/cem/ohpcf/usecase_test.go b/usecases/cem/ohpcf/usecase_test.go new file mode 100644 index 00000000..47c97480 --- /dev/null +++ b/usecases/cem/ohpcf/usecase_test.go @@ -0,0 +1,5 @@ +package ohpcf + +func (s *CemOHPCFSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +}