-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add OHPCF support as CEM client actor #122
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please add tests for this file as well? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is not part of the API interface. Was this missed? If so, I'd love to see some tests here as well. Additionally I would love use a different data structure instead of requiring the SPINE data model to be passed. |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Events for OHPCF data being updated should be added, similarly to how it is done in the OPEV implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure - once the other use cases are working to allow OHPCF to be triggered/activated ill add the events in. I guess OHPCF should have been the last use case to be implemented not the first! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now the API tries to avoid returning SPINE data models to the application, because these models are way too complex especially in this case. Instead it would be great to provide a simplified data structure which contains only the data required by the application.
Would it be possible to for you to design such a data structure here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed! Im not 100% sure how this structure should look like, and one of the issues is I cant fully use OHPCF until other use cases are completed (we need to be able to set temperature and configure the mode at least).
Once these other usecases are completed and I have a better idea of what the top level API should look like i'll update this.