Skip to content
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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions usecases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions usecases/api/cem_ohpcf.go
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)
Copy link
Member

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?

Copy link
Author

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.

}
32 changes: 32 additions & 0 deletions usecases/cem/ohpcf/events.go
Copy link
Member

Choose a reason for hiding this comment

The 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)
}
}
82 changes: 82 additions & 0 deletions usecases/cem/ohpcf/public.go
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) {

Check failure on line 25 in usecases/cem/ohpcf/public.go

View workflow job for this annotation

GitHub Actions / Build

unnecessary leading newline (whitespace)

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,
Copy link
Member

Choose a reason for hiding this comment

The 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) {

Check failure on line 65 in usecases/cem/ohpcf/public.go

View workflow job for this annotation

GitHub Actions / Build

unnecessary leading newline (whitespace)

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
}
48 changes: 48 additions & 0 deletions usecases/cem/ohpcf/public_test.go
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)
}
172 changes: 172 additions & 0 deletions usecases/cem/ohpcf/testhelper_test.go
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]
}
10 changes: 10 additions & 0 deletions usecases/cem/ohpcf/types.go
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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"
)
Loading
Loading