Skip to content

Commit

Permalink
Feat: Add OHPCF read and write
Browse files Browse the repository at this point in the history
  • Loading branch information
heavyweight87 committed Oct 14, 2024
1 parent 2a6167e commit 67cf8a1
Show file tree
Hide file tree
Showing 9 changed files with 438 additions and 0 deletions.
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)
}
32 changes: 32 additions & 0 deletions usecases/cem/ohpcf/events.go
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 an EV 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,
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
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

0 comments on commit 67cf8a1

Please sign in to comment.