From 523bd384b5ba6d7b51787fe40e2935ca8855d510 Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Fri, 28 Feb 2025 15:41:18 +0100 Subject: [PATCH 01/12] Initial --- usecases/api/cem_ohpcf.go | 75 ++++++ usecases/api/types.go | 11 + usecases/cem/ohpcf/events.go | 108 ++++++++ usecases/cem/ohpcf/events_test.go | 162 ++++++++++++ usecases/cem/ohpcf/public.go | 339 +++++++++++++++++++++++++ usecases/cem/ohpcf/public_test.go | 351 ++++++++++++++++++++++++++ usecases/cem/ohpcf/testhelper_test.go | 171 +++++++++++++ usecases/cem/ohpcf/types.go | 35 +++ usecases/cem/ohpcf/usecase.go | 66 +++++ usecases/cem/ohpcf/usecase_test.go | 5 + 10 files changed, 1323 insertions(+) create mode 100644 usecases/api/cem_ohpcf.go create mode 100644 usecases/cem/ohpcf/events.go create mode 100644 usecases/cem/ohpcf/events_test.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/api/cem_ohpcf.go b/usecases/api/cem_ohpcf.go new file mode 100644 index 00000000..9eae754a --- /dev/null +++ b/usecases/api/cem_ohpcf.go @@ -0,0 +1,75 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "time" +) + +type CemOHPCFInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // The availability of an optional consumption of power [OHPCF-011/1]. + // + // return true if the optional consumption of power is available + OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteInterface) (bool, error) + + // The power value [OHPCF-011/2/1 or 2]. + // + // return the power value + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Indication whether the consumption may be stopped by the CEM [OHPCF-011/5]. + // + // return true if the consumption may be stopped + ConsumptionIsStoppable(entity spineapi.EntityRemoteInterface) (bool, error) + + // Indication whether the consumption may be paused and resumed by the CEM [OHPCF-011/6] + // + // return true if the consumption may be paused + ConsumptionIsPausable(entity spineapi.EntityRemoteInterface) (bool, error) + + // The start time of the process [OHPCF-012/1]. + // + // return the start time of the process + PowerConsumptionProcessStartTime(entity spineapi.EntityRemoteInterface) (time.Time, error) + + // The current state of this power consumption process [OHPCF-012/2]. + // + // return the current state of this power consumption process + PowerConsumptionProcessState(entity spineapi.EntityRemoteInterface) (CompressorPowerConsumptionStateType, error) + + // The minimal time a consumption process must last [OHPCF-008]. + // + // return the minimal time a consumption process must last + PowerConsumptionMinimalRunDuration(entity spineapi.EntityRemoteInterface) (time.Duration, error) + + // The minimal time a pause of a consumption process must last [OHPCF-009]. + // + // return the minimal time a pause of a consumption process must last + PowerConsumptionMinimalPauseDuration(entity spineapi.EntityRemoteInterface) (time.Duration, error) + + // Scenario 2 + + // Schedule an optional power consumption process [OHPCF-004]. + // + // note: + // A re-schedule of an already scheduled power consumption process is possible as long as the + // scheduled process did not start. + // + // parameters: + // - start: The start time of the power consumption + SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + + // stop (abort) the process [OHPCF-022/1]. + StopAbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + + // pause the process [OHPCF-022/2]. + PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + + // resume the process [OHPCF-022/3]. + ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) +} diff --git a/usecases/api/types.go b/usecases/api/types.go index 52a7f54e..50c6c8b7 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -166,3 +166,14 @@ type DurationSlotValue struct { Duration time.Duration // Duration of this slot Value float64 // Energy Cost or Power Limit } + +type CompressorPowerConsumptionStateType string + +const ( + CompressorPowerConsumptionStateAvailable CompressorPowerConsumptionStateType = "available" + CompressorPowerConsumptionStateScheduled CompressorPowerConsumptionStateType = "scheduled" + CompressorPowerConsumptionStateRunning CompressorPowerConsumptionStateType = "running" + CompressorPowerConsumptionStatePaused CompressorPowerConsumptionStateType = "paused" + CompressorPowerConsumptionStateCompleted CompressorPowerConsumptionStateType = "completed" + CompressorPowerConsumptionStateStopped CompressorPowerConsumptionStateType = "stopped" // (aborted) +) diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go new file mode 100644 index 00000000..912c03a6 --- /dev/null +++ b/usecases/cem/ohpcf/events.go @@ -0,0 +1,108 @@ +package ohpcf + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !o.IsCompatibleEntityType(payload.Entity) { + return + } + + if payload.Data == nil { + return + } + + switch payload.Data.(type) { + case *model.SmartEnergyManagementPsDataType: + o.loadSmartEnergyManagementPsDataType(payload) + break + } +} + +func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayload) { + data := payload.Data.(*model.SmartEnergyManagementPsDataType) + + if len(data.Alternatives) == 1 && + len(data.Alternatives[0].PowerSequence) == 1 && + o.EventCB != nil { + + if len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && + len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value != nil { + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsStoppable != nil { + // [OHPCF-011/5] + // [OHPCF-012/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsStoppable) + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsPausable != nil { + // [OHPCF-011/6] + // [OHPCF-012/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsPausable) + } + + if data.Alternatives[0].PowerSequence[0].Schedule != nil && + data.Alternatives[0].PowerSequence[0].Schedule.StartTime != nil { + // [OHPCF-012/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime) + } + + if data.Alternatives[0].PowerSequence[0].State != nil && + data.Alternatives[0].PowerSequence[0].State.State != nil { + // [OHPCF-012/2] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState) + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive { + // [OHPCF-011/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionAvailable) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeScheduled { + // [OHPCF-012/2/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionScheduled) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeRunning { + //[OHPCF-012/2/2], [OHPCF-012/4], [OHPCF-022/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionRunning) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypePaused { + // [OHPCF-012/2/3], [OHPCF-022/2] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionPaused) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeCompleted { + // [OHPCF-006/3], [OHPCF-012/2/5] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionCompleted) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInvalid { + // [OHPCF-006/1], [OHPCF-012/2/4], [OHPCF-022/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) + } + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration != nil { + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.ActiveDurationMin != nil { + // [OHPCF-008] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration) + } + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.PauseDurationMin != nil { + // [OHPCF-009] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalPauseDuration) + } + } + } +} diff --git a/usecases/cem/ohpcf/events_test.go b/usecases/cem/ohpcf/events_test.go new file mode 100644 index 00000000..2500e934 --- /dev/null +++ b/usecases/cem/ohpcf/events_test.go @@ -0,0 +1,162 @@ +package ohpcf + +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" + "time" +) + +func (s *CemOhPCFSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + 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.SmartEnergyManagementPsDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *CemOhPCFSuite) Test_loadSmartEnergyManagementPsDataType() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeInactive), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1004), + }}, + }, + }}, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsStoppable: util.Ptr(true), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsPausable: util.Ptr(true), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Schedule: &model.PowerSequenceScheduleDataType{ + StartTime: model.NewAbsoluteOrRelativeTimeTypeFromTime(time.Time{}), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeInvalid), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsDuration: &model.OperatingConstraintsDurationDataType{ + ActiveDurationMin: model.NewDurationType(1000), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsDuration: &model.OperatingConstraintsDurationDataType{ + PauseDurationMin: model.NewDurationType(1000), + }, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go new file mode 100644 index 00000000..e0810f29 --- /dev/null +++ b/usecases/cem/ohpcf/public.go @@ -0,0 +1,339 @@ +package ohpcf + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "time" +) + +// Scenario 1 + +// The availability of an optional consumption of power [OHPCF-011/1]. +// +// return true if the optional consumption of power is available +func (o *OHPCF) OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteInterface) (bool, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return false, err + } + + if !o.isDataAvailable(data) { + return false, api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].State != nil && + data.Alternatives[0].PowerSequence[0].State.State != nil { + return *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive, nil + } + + return false, api.ErrDataNotAvailable +} + +// The power value [OHPCF-011/2/1 or 2]. +// +// return the power value +func (o *OHPCF) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return 0, err + } + + if !o.isDataAvailable(data) { + return 0, api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].PowerTimeSlot != nil && + len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value != nil && + len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value != nil { + return data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value.GetValue(), nil + } + + return 0, api.ErrDataNotAvailable +} + +// Indication whether the consumption may be stopped by the CEM [OHPCF-011/5]. +// +// return true if the consumption may be stopped +func (o *OHPCF) ConsumptionIsStoppable(entity spineapi.EntityRemoteInterface) (bool, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return false, err + } + + if !o.isDataAvailable(data) { + return false, api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsStoppable != nil { + return *data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsStoppable, nil + } + + return false, api.ErrDataNotAvailable +} + +// Indication whether the consumption may be paused and resumed by the CEM [OHPCF-011/6] +// +// return true if the consumption may be paused +func (o *OHPCF) ConsumptionIsPausable(entity spineapi.EntityRemoteInterface) (bool, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return false, err + } + + if !o.isDataAvailable(data) { + return false, api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsPausable != nil { + return *data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsPausable, nil + } + + return false, api.ErrDataNotAvailable +} + +// The start time of the process [OHPCF-012/1]. +// +// return the start time of the process +func (o *OHPCF) PowerConsumptionProcessStartTime(entity spineapi.EntityRemoteInterface) (time.Time, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return time.Time{}, err + } + + if !o.isDataAvailable(data) { + return time.Time{}, api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].Schedule != nil && + data.Alternatives[0].PowerSequence[0].Schedule.StartTime != nil { + return data.Alternatives[0].PowerSequence[0].Schedule.StartTime.GetTime() + } + + return time.Time{}, api.ErrDataNotAvailable +} + +// The current state of this power consumption process [OHPCF-012/2]. +// +// return the current state of this power consumption process +func (o *OHPCF) PowerConsumptionProcessState(entity spineapi.EntityRemoteInterface) (ucapi.CompressorPowerConsumptionStateType, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return "", err + } + + if !o.isDataAvailable(data) { + return "", api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].State != nil && + data.Alternatives[0].PowerSequence[0].State.State != nil { + switch *data.Alternatives[0].PowerSequence[0].State.State { + case model.PowerSequenceStateTypeInactive: + return ucapi.CompressorPowerConsumptionStateAvailable, nil + case model.PowerSequenceStateTypeScheduled: + return ucapi.CompressorPowerConsumptionStateScheduled, nil + case model.PowerSequenceStateTypeRunning: + return ucapi.CompressorPowerConsumptionStateRunning, nil + case model.PowerSequenceStateTypeScheduledPaused: + return ucapi.CompressorPowerConsumptionStatePaused, nil + case model.PowerSequenceStateTypeCompleted: + return ucapi.CompressorPowerConsumptionStateCompleted, nil + case model.PowerSequenceStateTypeInvalid: + return ucapi.CompressorPowerConsumptionStateStopped, nil + default: + return "", api.ErrNotSupported + } + } + + return "", api.ErrDataNotAvailable +} + +// The minimal time a consumption process must last [OHPCF-008]. +// +// return the minimal time a consumption process must last +func (o *OHPCF) PowerConsumptionMinimalRunDuration(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return time.Duration(0), err + } + + if !o.isDataAvailable(data) { + return time.Duration(0), api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.ActiveDurationMin != nil { + return data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.ActiveDurationMin.GetTimeDuration() + } + + return time.Duration(0), api.ErrDataNotAvailable +} + +// The minimal time a pause of a consumption process must last [OHPCF-009]. +// +// return the minimal time a pause of a consumption process must last +func (o *OHPCF) PowerConsumptionMinimalPauseDuration(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return time.Duration(0), err + } + + if !o.isDataAvailable(data) { + return time.Duration(0), api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.PauseDurationMin != nil { + return data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.PauseDurationMin.GetTimeDuration() + } + + return time.Duration(0), api.ErrDataNotAvailable +} + +// Scenario 2 + +// Schedule an optional power consumption process [OHPCF-004]. +// +// note: +// A re-schedule of an already scheduled power consumption process is possible as long as the +// scheduled process did not start. +// +// parameters: +// - start: The start time of the power consumption +func (o *OHPCF) SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { + if !o.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) + if err != nil { + return nil, err + } + + if callback != nil { + smartEnergyManagementPs.AddResultCallback(callback) + } + + return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Schedule: &model.PowerSequenceScheduleDataType{ + StartTime: model.NewAbsoluteOrRelativeTimeTypeFromTime(start), + }, + }}, + }}, + }) +} + +// stop (abort) the process [OHPCF-022/1]. +func (o *OHPCF) StopAbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { + if !o.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) + if err != nil { + return nil, err + } + + if callback != nil { + smartEnergyManagementPs.AddResultCallback(callback) + } + + return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeInvalid), + }, + }}, + }}, + }) +} + +// pause the process [OHPCF-022/2]. +func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { + if !o.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) + if err != nil { + return nil, err + } + + if callback != nil { + smartEnergyManagementPs.AddResultCallback(callback) + } + + return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeScheduledPaused), + }, + }}, + }}, + }) +} + +// resume the process [OHPCF-022/3]. +func (o *OHPCF) ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { + if !o.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) + if err != nil { + return nil, err + } + + if callback != nil { + smartEnergyManagementPs.AddResultCallback(callback) + } + + return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeRunning), + }, + }}, + }}, + }) +} + +// ------------------------ helper methods ------------------------ // + +func (o *OHPCF) checkEntityTypeAndGetData(entity spineapi.EntityRemoteInterface) (*model.SmartEnergyManagementPsDataType, error) { + if !o.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) + if err != nil { + return nil, err + } + + data, err := smartEnergyManagementPs.GetData() + if err != nil { + return nil, err + } + + return data, nil +} + +func (o *OHPCF) isDataAvailable(data *model.SmartEnergyManagementPsDataType) bool { + return len(data.Alternatives) == 1 && + data.Alternatives[0].PowerSequence != nil && + len(data.Alternatives[0].PowerSequence) == 1 +} diff --git a/usecases/cem/ohpcf/public_test.go b/usecases/cem/ohpcf/public_test.go new file mode 100644 index 00000000..bbb27f5b --- /dev/null +++ b/usecases/cem/ohpcf/public_test.go @@ -0,0 +1,351 @@ +package ohpcf + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" + "time" +) + +// Scenario 1 + +func (s *CemOhPCFSuite) Test_OptionalPowerConsumptionAvailable() { + _, err := s.sut.OptionalPowerConsumptionAvailable(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.OptionalPowerConsumptionAvailable(s.monitoredEntity) + assert.NotNil(s.T(), err) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeInactive), + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + available, err := s.sut.OptionalPowerConsumptionAvailable(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, available) +} + +func (s *CemOhPCFSuite) Test_Power() { + _, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1004), + }}, + }, + }}, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + available, err := s.sut.Power(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1004.0, available) +} + +func (s *CemOhPCFSuite) Test_ConsumptionIsStoppable() { + _, err := s.sut.ConsumptionIsStoppable(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ConsumptionIsStoppable(s.monitoredEntity) + assert.NotNil(s.T(), err) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsStoppable: util.Ptr(true), + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + stoppable, err := s.sut.ConsumptionIsStoppable(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, stoppable) +} + +func (s *CemOhPCFSuite) Test_ConsumptionIsPausable() { + _, err := s.sut.ConsumptionIsPausable(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ConsumptionIsPausable(s.monitoredEntity) + assert.NotNil(s.T(), err) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsPausable: util.Ptr(true), + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + pausable, err := s.sut.ConsumptionIsPausable(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, pausable) +} + +func (s *CemOhPCFSuite) Test_PowerConsumptionProcessStartTime() { + _, err := s.sut.PowerConsumptionProcessStartTime(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.PowerConsumptionProcessStartTime(s.monitoredEntity) + assert.NotNil(s.T(), err) + + utcNow := time.Now().UTC() + utcNowTimeObj := model.NewAbsoluteOrRelativeTimeTypeFromTime(utcNow) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Schedule: &model.PowerSequenceScheduleDataType{ + StartTime: utcNowTimeObj, + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + startTime, err := s.sut.PowerConsumptionProcessStartTime(s.monitoredEntity) + assert.Nil(s.T(), err) + expected, _ := utcNowTimeObj.GetTime() + assert.Equal(s.T(), expected, startTime) +} + +func (s *CemOhPCFSuite) Test_PowerConsumptionProcessState() { + _, err := s.sut.PowerConsumptionProcessState(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.PowerConsumptionProcessState(s.monitoredEntity) + assert.NotNil(s.T(), err) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeInactive), + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + state, err := s.sut.PowerConsumptionProcessState(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), "inactive", state) +} + +func (s *CemOhPCFSuite) Test_PowerConsumptionMinimalRunDuration() { + _, err := s.sut.PowerConsumptionMinimalRunDuration(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.PowerConsumptionMinimalRunDuration(s.monitoredEntity) + assert.NotNil(s.T(), err) + + duration := time.Duration(120000000000000000) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsDuration: &model.OperatingConstraintsDurationDataType{ + ActiveDurationMin: model.NewDurationType(duration), + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + minDuration, err := s.sut.PowerConsumptionMinimalRunDuration(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), duration, minDuration) +} + +func (s *CemOhPCFSuite) Test_PowerConsumptionMinimalPauseDuration() { + _, err := s.sut.PowerConsumptionMinimalPauseDuration(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.PowerConsumptionMinimalPauseDuration(s.monitoredEntity) + assert.NotNil(s.T(), err) + + duration := time.Duration(120000000000000000) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + OperatingConstraintsDuration: &model.OperatingConstraintsDurationDataType{ + PauseDurationMin: model.NewDurationType(duration), + }, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + minDuration, err := s.sut.PowerConsumptionMinimalPauseDuration(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), duration, minDuration) +} + +// Scenario 2 + +func (s *CemOhPCFSuite) Test_SchedulePowerConsumptionProcess() { + _, err := s.sut.SchedulePowerConsumptionProcess(s.mockRemoteEntity, time.Now(), nil) + assert.NotNil(s.T(), err) + + msgCounter, err := s.sut.SchedulePowerConsumptionProcess(s.monitoredEntity, time.Now(), nil) + assert.NotNil(s.T(), msgCounter) + assert.Nil(s.T(), err) +} + +func (s *CemOhPCFSuite) Test_StopAbortPowerConsumptionProcess() { + _, err := s.sut.StopAbortPowerConsumptionProcess(s.mockRemoteEntity, nil) + assert.NotNil(s.T(), err) + + msgCounter, err := s.sut.StopAbortPowerConsumptionProcess(s.monitoredEntity, nil) + assert.NotNil(s.T(), msgCounter) + assert.Nil(s.T(), err) +} + +func (s *CemOhPCFSuite) Test_PausePowerConsumptionProcess() { + _, err := s.sut.PausePowerConsumptionProcess(s.mockRemoteEntity, nil) + assert.NotNil(s.T(), err) + + msgCounter, err := s.sut.PausePowerConsumptionProcess(s.monitoredEntity, nil) + assert.NotNil(s.T(), msgCounter) + assert.Nil(s.T(), err) +} + +func (s *CemOhPCFSuite) Test_ResumePowerConsumptionProcess() { + _, err := s.sut.ResumePowerConsumptionProcess(s.mockRemoteEntity, nil) + assert.NotNil(s.T(), err) + + msgCounter, err := s.sut.ResumePowerConsumptionProcess(s.monitoredEntity, nil) + assert.NotNil(s.T(), msgCounter) + assert.Nil(s.T(), err) +} + +// Helper methods + +func (s *CemOhPCFSuite) Test_checkEntityTypeAndGetData() { + _, err := s.sut.checkEntityTypeAndGetData(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), err) + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, &model.SmartEnergyManagementPsDataType{}, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), data) + assert.Nil(s.T(), err) +} + +func (s *CemOhPCFSuite) Test_isDataAvailable() { + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, &model.SmartEnergyManagementPsDataType{}, nil, nil) + assert.Nil(s.T(), fErr) + + fData, err := s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), fData) + assert.Nil(s.T(), err) + + available := s.sut.isDataAvailable(fData) + assert.Equal(s.T(), false, available) + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{}, + }, nil, nil) + assert.Nil(s.T(), fErr) + + fData, err = s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), fData) + assert.Nil(s.T(), err) + + available = s.sut.isDataAvailable(fData) + assert.Equal(s.T(), false, available) + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{}}, + }, nil, nil) + assert.Nil(s.T(), fErr) + + fData, err = s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), fData) + assert.Nil(s.T(), err) + + available = s.sut.isDataAvailable(fData) + assert.Equal(s.T(), false, available) + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{}, + }}, + }, nil, nil) + assert.Nil(s.T(), fErr) + + fData, err = s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), fData) + assert.Nil(s.T(), err) + + available = s.sut.isDataAvailable(fData) + assert.Equal(s.T(), false, available) + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{}}, + }}, + }, nil, nil) + assert.Nil(s.T(), fErr) + + fData, err = s.sut.checkEntityTypeAndGetData(s.monitoredEntity) + assert.NotNil(s.T(), fData) + assert.Nil(s.T(), err) + + available = s.sut.isDataAvailable(fData) + assert.Equal(s.T(), true, available) +} diff --git a/usecases/cem/ohpcf/testhelper_test.go b/usecases/cem/ohpcf/testhelper_test.go new file mode 100644 index 00000000..4db30275 --- /dev/null +++ b/usecases/cem/ohpcf/testhelper_test.go @@ -0,0 +1,171 @@ +package ohpcf + +import ( + "fmt" + "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" + "testing" + "time" +) + +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..c4f75af2 --- /dev/null +++ b/usecases/cem/ohpcf/types.go @@ -0,0 +1,35 @@ +package ohpcf + +import "github.com/enbility/eebus-go/api" + +const ( + UseCaseSupportUpdate api.EventType = "cem-ohpcf-UseCaseSupportUpdate" + + // Scenario 1 + + DataUpdatePower api.EventType = "cem-ohpcf-DataUpdatePowerChanged" + + DataUpdateConsumptionIsStoppable api.EventType = "cem-ohpcf-DataUpdateConsumptionIsStoppable" + + DataUpdateConsumptionIsPausable api.EventType = "cem-ohpcf-DataUpdateConsumptionIsPausable" + + DataUpdateConsumptionStartTime api.EventType = "cem-ohpcf-DataUpdateConsumptionStartTime" + + DataUpdateConsumptionState api.EventType = "cem-ohpcf-DataUpdateConsumptionState" + + DataUpdateOptionalPowerConsumptionAvailable api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionAvailable" + + DataUpdateOptionalPowerConsumptionScheduled api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionScheduled" + + DataUpdateOptionalPowerConsumptionRunning api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionRunning" + + DataUpdateOptionalPowerConsumptionPaused api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionPaused" + + DataUpdateOptionalPowerConsumptionCompleted api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionCompleted" + + DataUpdateOptionalPowerConsumptionStopped api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionStopped" + + DataUpdateMinimalRunDuration api.EventType = "cem-ohpcf-DataUpdateMinimalRunDuration" + + DataUpdateMinimalPauseDuration api.EventType = "cem-ohpcf-DataUpdateMinimalPauseDuration" +) diff --git a/usecases/cem/ohpcf/usecase.go b/usecases/cem/ohpcf/usecase.go new file mode 100644 index 00000000..8734795e --- /dev/null +++ b/usecases/cem/ohpcf/usecase.go @@ -0,0 +1,66 @@ +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" +) + +type OHPCF struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemOHPCFInterface = (*OHPCF)(nil) + +func NewOHPCF(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *OHPCF { + validActorTypes := []model.UseCaseActorType{ + model.UseCaseActorTypeCompressor, + } + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + } + useCaseScenarios := []api.UseCaseScenario{ + { + Scenario: model.UseCaseScenarioSupportType(1), + Mandatory: true, + }, + { + Scenario: model.UseCaseScenarioSupportType(2), + Mandatory: true, + }, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeEVStateOfCharge, + "1.0.0", + "RC1", + useCaseScenarios, + eventCB, + UseCaseSupportUpdate, + validActorTypes, + validEntityTypes, + ) + + uc := &OHPCF{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (o *OHPCF) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeSmartEnergyManagementPs, + } + for _, feature := range clientFeatures { + _ = o.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..2f5df814 --- /dev/null +++ b/usecases/cem/ohpcf/usecase_test.go @@ -0,0 +1,5 @@ +package ohpcf + +func (s *CemOhPCFSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} From a6189fb87f616c990690aa7e446006ede1da6139 Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Fri, 28 Feb 2025 15:57:04 +0100 Subject: [PATCH 02/12] additional event handling for abortion (stop-event) and better error handling for OptionalPowerConsumptionAvailable -> no error if data not available --- usecases/cem/ohpcf/events.go | 134 ++++++++++++++++++----------------- usecases/cem/ohpcf/public.go | 4 +- 2 files changed, 72 insertions(+), 66 deletions(-) diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go index 912c03a6..83d0f51b 100644 --- a/usecases/cem/ohpcf/events.go +++ b/usecases/cem/ohpcf/events.go @@ -27,81 +27,87 @@ func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayload) { data := payload.Data.(*model.SmartEnergyManagementPsDataType) - if len(data.Alternatives) == 1 && - len(data.Alternatives[0].PowerSequence) == 1 && - o.EventCB != nil { - - if len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && - len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value) == 1 && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value != nil { - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) - } - - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && - data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsStoppable != nil { - // [OHPCF-011/5] - // [OHPCF-012/3] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsStoppable) - } - - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && - data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsPausable != nil { - // [OHPCF-011/6] - // [OHPCF-012/3] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsPausable) - } - - if data.Alternatives[0].PowerSequence[0].Schedule != nil && - data.Alternatives[0].PowerSequence[0].Schedule.StartTime != nil { - // [OHPCF-012/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime) - } - - if data.Alternatives[0].PowerSequence[0].State != nil && - data.Alternatives[0].PowerSequence[0].State.State != nil { - // [OHPCF-012/2] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState) - - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive { - // [OHPCF-011/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionAvailable) + if len(data.Alternatives) == 1 { + if len(data.Alternatives[0].PowerSequence) == 1 && + o.EventCB != nil { + + if len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && + len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value != nil { + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) } - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeScheduled { - // [OHPCF-012/2/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionScheduled) + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsStoppable != nil { + // [OHPCF-011/5] + // [OHPCF-012/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsStoppable) } - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeRunning { - //[OHPCF-012/2/2], [OHPCF-012/4], [OHPCF-022/3] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionRunning) + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && + data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsPausable != nil { + // [OHPCF-011/6] + // [OHPCF-012/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsPausable) } - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypePaused { - // [OHPCF-012/2/3], [OHPCF-022/2] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionPaused) + if data.Alternatives[0].PowerSequence[0].Schedule != nil && + data.Alternatives[0].PowerSequence[0].Schedule.StartTime != nil { + // [OHPCF-012/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime) } - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeCompleted { - // [OHPCF-006/3], [OHPCF-012/2/5] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionCompleted) + if data.Alternatives[0].PowerSequence[0].State != nil && + data.Alternatives[0].PowerSequence[0].State.State != nil { + // [OHPCF-012/2] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState) + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive { + // [OHPCF-011/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionAvailable) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeScheduled { + // [OHPCF-012/2/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionScheduled) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeRunning { + //[OHPCF-012/2/2], [OHPCF-012/4], [OHPCF-022/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionRunning) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypePaused { + // [OHPCF-012/2/3], [OHPCF-022/2] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionPaused) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeCompleted { + // [OHPCF-006/3], [OHPCF-012/2/5] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionCompleted) + } + + if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInvalid { + // [OHPCF-006/1], [OHPCF-012/2/4], [OHPCF-022/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) + } } - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInvalid { - // [OHPCF-006/1], [OHPCF-012/2/4], [OHPCF-022/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration != nil { + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.ActiveDurationMin != nil { + // [OHPCF-008] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration) + } + if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.PauseDurationMin != nil { + // [OHPCF-009] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalPauseDuration) + } } - } - - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration != nil { - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.ActiveDurationMin != nil { - // [OHPCF-008] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration) - } - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.PauseDurationMin != nil { - // [OHPCF-009] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalPauseDuration) + } else { + if o.EventCB != nil { + // [OHPCF-003], [OHPCF-006/2] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) } } } diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index e0810f29..c97fcea2 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -22,7 +22,7 @@ func (o *OHPCF) OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteIn } if !o.isDataAvailable(data) { - return false, api.ErrDataNotAvailable + return false, nil } if data.Alternatives[0].PowerSequence[0].State != nil && @@ -30,7 +30,7 @@ func (o *OHPCF) OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteIn return *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive, nil } - return false, api.ErrDataNotAvailable + return false, nil } // The power value [OHPCF-011/2/1 or 2]. From 38a1871802d5ee45833b824b6d6d584e304582f0 Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Mon, 17 Mar 2025 11:44:04 +0100 Subject: [PATCH 03/12] Resolve Changes --- usecases/api/cem_ohpcf.go | 25 +++-- usecases/cem/ohpcf/events.go | 115 +++++++++---------- usecases/cem/ohpcf/events_test.go | 23 +++- usecases/cem/ohpcf/public.go | 176 +++++++++++++++--------------- usecases/cem/ohpcf/public_test.go | 47 ++++++-- usecases/cem/ohpcf/types.go | 14 +-- usecases/cem/ohpcf/usecase.go | 4 +- 7 files changed, 222 insertions(+), 182 deletions(-) diff --git a/usecases/api/cem_ohpcf.go b/usecases/api/cem_ohpcf.go index 9eae754a..8b6009bb 100644 --- a/usecases/api/cem_ohpcf.go +++ b/usecases/api/cem_ohpcf.go @@ -14,13 +14,18 @@ type CemOHPCFInterface interface { // The availability of an optional consumption of power [OHPCF-011/1]. // - // return true if the optional consumption of power is available + // return true if the optional consumption of power is possible OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteInterface) (bool, error) - // The power value [OHPCF-011/2/1 or 2]. + // The requested power estimate value [OHPCF-011/2/1]. // - // return the power value - Power(entity spineapi.EntityRemoteInterface) (float64, error) + // return the requested power estimate value + RequestedPowerEstimate(entity spineapi.EntityRemoteInterface) (float64, error) + + // The maximal value for the requested power estimate [OHPCF-011/2/1]. + // + // return the maximal value for the requested power estimate + RequestPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) // Indication whether the consumption may be stopped by the CEM [OHPCF-011/5]. // @@ -57,19 +62,19 @@ type CemOHPCFInterface interface { // Schedule an optional power consumption process [OHPCF-004]. // // note: - // A re-schedule of an already scheduled power consumption process is possible as long as the - // scheduled process did not start. + // Rescheduling an already scheduled power consumption process is possible as long as the + // scheduled process has not startet yet. // // parameters: // - start: The start time of the power consumption - SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) // stop (abort) the process [OHPCF-022/1]. - StopAbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + AbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) // pause the process [OHPCF-022/2]. - PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) // resume the process [OHPCF-022/3]. - ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) + ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) } diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go index 83d0f51b..3c8ec5e1 100644 --- a/usecases/cem/ohpcf/events.go +++ b/usecases/cem/ohpcf/events.go @@ -27,88 +27,73 @@ func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayload) { data := payload.Data.(*model.SmartEnergyManagementPsDataType) + if o.EventCB == nil { + return + } + if len(data.Alternatives) == 1 { - if len(data.Alternatives[0].PowerSequence) == 1 && - o.EventCB != nil { - - if len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && - len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value) == 1 && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value != nil { - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + alternative := data.Alternatives[0] + + if len(alternative.PowerSequence) != 1 { + return + } + + request := alternative.PowerSequence[0] + + if len(request.PowerTimeSlot) == 1 && + request.PowerTimeSlot[0].ValueList != nil && + len(request.PowerTimeSlot[0].ValueList.Value) > 0 { + for _, value := range request.PowerTimeSlot[0].ValueList.Value { + if value.Value != nil && + value.ValueType != nil { + if *value.ValueType == model.PowerTimeSlotValueTypeTypePower { + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } else if *value.ValueType == model.PowerTimeSlotValueTypeTypePowerMax { + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMaxPower) + } + } } + } - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && - data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsStoppable != nil { + if request.OperatingConstraintsInterrupt != nil { + if request.OperatingConstraintsInterrupt.IsStoppable != nil { // [OHPCF-011/5] // [OHPCF-012/3] o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsStoppable) } - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt != nil && - data.Alternatives[0].PowerSequence[0].OperatingConstraintsInterrupt.IsPausable != nil { + if request.OperatingConstraintsInterrupt.IsPausable != nil { // [OHPCF-011/6] // [OHPCF-012/3] o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsPausable) } + } - if data.Alternatives[0].PowerSequence[0].Schedule != nil && - data.Alternatives[0].PowerSequence[0].Schedule.StartTime != nil { - // [OHPCF-012/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime) - } - - if data.Alternatives[0].PowerSequence[0].State != nil && - data.Alternatives[0].PowerSequence[0].State.State != nil { - // [OHPCF-012/2] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState) - - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive { - // [OHPCF-011/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionAvailable) - } - - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeScheduled { - // [OHPCF-012/2/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionScheduled) - } - - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeRunning { - //[OHPCF-012/2/2], [OHPCF-012/4], [OHPCF-022/3] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionRunning) - } - - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypePaused { - // [OHPCF-012/2/3], [OHPCF-022/2] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionPaused) - } - - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeCompleted { - // [OHPCF-006/3], [OHPCF-012/2/5] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionCompleted) - } + if request.Schedule != nil && + request.Schedule.StartTime != nil { + // [OHPCF-012/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime) + } - if *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInvalid { - // [OHPCF-006/1], [OHPCF-012/2/4], [OHPCF-022/1] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) - } - } + if request.State != nil && + request.State.State != nil { + // [OHPCF-006], [OHPCF-011/1], [OHPCF-012/2], [OHPCF-022] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState) + } - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration != nil { - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.ActiveDurationMin != nil { - // [OHPCF-008] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration) - } - if data.Alternatives[0].PowerSequence[0].OperatingConstraintsDuration.PauseDurationMin != nil { - // [OHPCF-009] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalPauseDuration) - } + if request.OperatingConstraintsDuration != nil { + if request.OperatingConstraintsDuration.ActiveDurationMin != nil { + // [OHPCF-008] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration) } - } else { - if o.EventCB != nil { - // [OHPCF-003], [OHPCF-006/2] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) + if request.OperatingConstraintsDuration.PauseDurationMin != nil { + // [OHPCF-009] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalPauseDuration) } } + + } else if len(data.Alternatives) == 0 { + // [OHPCF-003], [OHPCF-006/2] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) } } diff --git a/usecases/cem/ohpcf/events_test.go b/usecases/cem/ohpcf/events_test.go index 2500e934..3172bcb4 100644 --- a/usecases/cem/ohpcf/events_test.go +++ b/usecases/cem/ohpcf/events_test.go @@ -59,7 +59,28 @@ func (s *CemOhPCFSuite) Test_loadSmartEnergyManagementPsDataType() { PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ Value: []model.PowerTimeSlotValueDataType{{ - Value: model.NewScaledNumberType(1004), + Value: model.NewScaledNumberType(1004), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePower), + }}, + }, + }}, + }}, + }}, + } + payload.Data = data + s.sut.loadSmartEnergyManagementPsDataType(payload) + assert.True(s.T(), s.eventCalled) + + s.eventCalled = false + + data = &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(10432), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePowerMax), }}, }, }}, diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index c97fcea2..6524ccae 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -4,6 +4,7 @@ import ( "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/features/client" ucapi "github.com/enbility/eebus-go/usecases/api" + "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" @@ -27,35 +28,25 @@ func (o *OHPCF) OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteIn if data.Alternatives[0].PowerSequence[0].State != nil && data.Alternatives[0].PowerSequence[0].State.State != nil { - return *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive, nil + return *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeInactive || + *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeScheduled, nil } return false, nil } -// The power value [OHPCF-011/2/1 or 2]. +// The power value [OHPCF-011/2/1]. // // return the power value -func (o *OHPCF) Power(entity spineapi.EntityRemoteInterface) (float64, error) { - data, err := o.checkEntityTypeAndGetData(entity) - if err != nil { - return 0, err - } - - if !o.isDataAvailable(data) { - return 0, api.ErrDataNotAvailable - } - - if data.Alternatives[0].PowerSequence[0].PowerTimeSlot != nil && - len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value != nil && - len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value) == 1 && - data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value != nil { - return data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value[0].Value.GetValue(), nil - } +func (o *OHPCF) RequestedPowerEstimate(entity spineapi.EntityRemoteInterface) (float64, error) { + return o.powerOfType(entity, model.PowerTimeSlotValueTypeTypePower) +} - return 0, api.ErrDataNotAvailable +// The max power value [OHPCF-011/2/2]. +// +// return the maximal power value +func (o *OHPCF) RequestPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) { + return o.powerOfType(entity, model.PowerTimeSlotValueTypeTypePowerMax) } // Indication whether the consumption may be stopped by the CEM [OHPCF-011/5]. @@ -63,6 +54,7 @@ func (o *OHPCF) Power(entity spineapi.EntityRemoteInterface) (float64, error) { // return true if the consumption may be stopped func (o *OHPCF) ConsumptionIsStoppable(entity spineapi.EntityRemoteInterface) (bool, error) { data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { return false, err } @@ -131,7 +123,7 @@ func (o *OHPCF) PowerConsumptionProcessState(entity spineapi.EntityRemoteInterfa } if !o.isDataAvailable(data) { - return "", api.ErrDataNotAvailable + return ucapi.CompressorPowerConsumptionStateStopped, nil } if data.Alternatives[0].PowerSequence[0].State != nil && @@ -209,21 +201,8 @@ func (o *OHPCF) PowerConsumptionMinimalPauseDuration(entity spineapi.EntityRemot // // parameters: // - start: The start time of the power consumption -func (o *OHPCF) SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { - if !o.IsCompatibleEntityType(entity) { - return nil, api.ErrNoCompatibleEntity - } - - smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) - if err != nil { - return nil, err - } - - if callback != nil { - smartEnergyManagementPs.AddResultCallback(callback) - } - - return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ +func (o *OHPCF) SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ Schedule: &model.PowerSequenceScheduleDataType{ @@ -231,25 +210,14 @@ func (o *OHPCF) SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInte }, }}, }}, - }) -} - -// stop (abort) the process [OHPCF-022/1]. -func (o *OHPCF) StopAbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { - if !o.IsCompatibleEntityType(entity) { - return nil, api.ErrNoCompatibleEntity - } - - smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) - if err != nil { - return nil, err } - if callback != nil { - smartEnergyManagementPs.AddResultCallback(callback) - } + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} - return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ +// stop (abort) the process [OHPCF-022/1]. +func (o *OHPCF) AbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ State: &model.PowerSequenceStateDataType{ @@ -257,25 +225,14 @@ func (o *OHPCF) StopAbortPowerConsumptionProcess(entity spineapi.EntityRemoteInt }, }}, }}, - }) -} - -// pause the process [OHPCF-022/2]. -func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { - if !o.IsCompatibleEntityType(entity) { - return nil, api.ErrNoCompatibleEntity } - smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) - if err != nil { - return nil, err - } - - if callback != nil { - smartEnergyManagementPs.AddResultCallback(callback) - } + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} - return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ +// pause the process [OHPCF-022/2]. +func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ State: &model.PowerSequenceStateDataType{ @@ -283,25 +240,14 @@ func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterfa }, }}, }}, - }) -} - -// resume the process [OHPCF-022/3]. -func (o *OHPCF) ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, callback func(msg spineapi.ResponseMessage)) (*model.MsgCounterType, error) { - if !o.IsCompatibleEntityType(entity) { - return nil, api.ErrNoCompatibleEntity - } - - smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) - if err != nil { - return nil, err } - if callback != nil { - smartEnergyManagementPs.AddResultCallback(callback) - } + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} - return smartEnergyManagementPs.WriteData(&model.SmartEnergyManagementPsDataType{ +// resume the process [OHPCF-022/3]. +func (o *OHPCF) ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ State: &model.PowerSequenceStateDataType{ @@ -309,7 +255,9 @@ func (o *OHPCF) ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterf }, }}, }}, - }) + } + + return o.writeSmartEnergyManagementData(entity, data, resultCB) } // ------------------------ helper methods ------------------------ // @@ -337,3 +285,59 @@ func (o *OHPCF) isDataAvailable(data *model.SmartEnergyManagementPsDataType) boo data.Alternatives[0].PowerSequence != nil && len(data.Alternatives[0].PowerSequence) == 1 } + +func (o *OHPCF) powerOfType(entity spineapi.EntityRemoteInterface, valueType model.PowerTimeSlotValueTypeType) (float64, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return 0, err + } + + if !o.isDataAvailable(data) { + return 0, api.ErrDataNotAvailable + } + + if data.Alternatives[0].PowerSequence[0].PowerTimeSlot != nil && + len(data.Alternatives[0].PowerSequence[0].PowerTimeSlot) == 1 && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList != nil && + data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value != nil { + + for _, value := range data.Alternatives[0].PowerSequence[0].PowerTimeSlot[0].ValueList.Value { + if value.Value != nil && value.ValueType != nil && *value.ValueType == valueType { + return value.Value.GetValue(), nil + } + } + } + + return 0, api.ErrDataNotAvailable +} + +func (o *OHPCF) writeSmartEnergyManagementData(entity spineapi.EntityRemoteInterface, data *model.SmartEnergyManagementPsDataType, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + if !o.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + smartEnergyManagementPs, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity) + if err != nil { + return nil, err + } + + msgCounter, err := smartEnergyManagementPs.WriteData(data) + + if err != nil { + return nil, err + } + + if resultCB != nil && msgCounter != nil { + cb := func(msg spineapi.ResponseMessage) { + response, ok := msg.Data.(*model.ResultDataType) + if ok { + resultCB(*response) + } + } + if errCB := smartEnergyManagementPs.AddResponseCallback(*msgCounter, cb); errCB != nil { + logging.Log().Debug("Failed to add response callback for msgCounter %v: %v", msgCounter, errCB) + } + } + + return msgCounter, nil +} diff --git a/usecases/cem/ohpcf/public_test.go b/usecases/cem/ohpcf/public_test.go index bbb27f5b..47c192eb 100644 --- a/usecases/cem/ohpcf/public_test.go +++ b/usecases/cem/ohpcf/public_test.go @@ -1,6 +1,7 @@ package ohpcf import ( + "github.com/enbility/eebus-go/usecases/api" "github.com/enbility/spine-go/model" "github.com/enbility/spine-go/util" "github.com/stretchr/testify/assert" @@ -36,10 +37,10 @@ func (s *CemOhPCFSuite) Test_OptionalPowerConsumptionAvailable() { } func (s *CemOhPCFSuite) Test_Power() { - _, err := s.sut.Power(s.mockRemoteEntity) + _, err := s.sut.RequestedPowerEstimate(s.mockRemoteEntity) assert.NotNil(s.T(), err) - _, err = s.sut.Power(s.monitoredEntity) + _, err = s.sut.RequestedPowerEstimate(s.monitoredEntity) assert.NotNil(s.T(), err) data := &model.SmartEnergyManagementPsDataType{ @@ -48,7 +49,8 @@ func (s *CemOhPCFSuite) Test_Power() { PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ Value: []model.PowerTimeSlotValueDataType{{ - Value: model.NewScaledNumberType(1004), + Value: model.NewScaledNumberType(1004), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePower), }}, }, }}, @@ -60,11 +62,42 @@ func (s *CemOhPCFSuite) Test_Power() { _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) assert.Nil(s.T(), fErr) - available, err := s.sut.Power(s.monitoredEntity) + available, err := s.sut.RequestedPowerEstimate(s.monitoredEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 1004.0, available) } +func (s *CemOhPCFSuite) Test_MaxPower() { + _, err := s.sut.RequestPowerMax(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.RequestPowerMax(s.monitoredEntity) + assert.NotNil(s.T(), err) + + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1006), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePowerMax), + }}, + }, + }}, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + available, err := s.sut.RequestPowerMax(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1006.0, available) +} + func (s *CemOhPCFSuite) Test_ConsumptionIsStoppable() { _, err := s.sut.ConsumptionIsStoppable(s.mockRemoteEntity) assert.NotNil(s.T(), err) @@ -170,7 +203,7 @@ func (s *CemOhPCFSuite) Test_PowerConsumptionProcessState() { state, err := s.sut.PowerConsumptionProcessState(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), "inactive", state) + assert.Equal(s.T(), api.CompressorPowerConsumptionStateAvailable, state) } func (s *CemOhPCFSuite) Test_PowerConsumptionMinimalRunDuration() { @@ -241,10 +274,10 @@ func (s *CemOhPCFSuite) Test_SchedulePowerConsumptionProcess() { } func (s *CemOhPCFSuite) Test_StopAbortPowerConsumptionProcess() { - _, err := s.sut.StopAbortPowerConsumptionProcess(s.mockRemoteEntity, nil) + _, err := s.sut.AbortPowerConsumptionProcess(s.mockRemoteEntity, nil) assert.NotNil(s.T(), err) - msgCounter, err := s.sut.StopAbortPowerConsumptionProcess(s.monitoredEntity, nil) + msgCounter, err := s.sut.AbortPowerConsumptionProcess(s.monitoredEntity, nil) assert.NotNil(s.T(), msgCounter) assert.Nil(s.T(), err) } diff --git a/usecases/cem/ohpcf/types.go b/usecases/cem/ohpcf/types.go index c4f75af2..8fe8f427 100644 --- a/usecases/cem/ohpcf/types.go +++ b/usecases/cem/ohpcf/types.go @@ -7,7 +7,9 @@ const ( // Scenario 1 - DataUpdatePower api.EventType = "cem-ohpcf-DataUpdatePowerChanged" + DataUpdatePower api.EventType = "cem-ohpcf-DataUpdatePower" + + DataUpdateMaxPower api.EventType = "cem-ohpcf-DataUpdateMaxPower" DataUpdateConsumptionIsStoppable api.EventType = "cem-ohpcf-DataUpdateConsumptionIsStoppable" @@ -17,16 +19,6 @@ const ( DataUpdateConsumptionState api.EventType = "cem-ohpcf-DataUpdateConsumptionState" - DataUpdateOptionalPowerConsumptionAvailable api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionAvailable" - - DataUpdateOptionalPowerConsumptionScheduled api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionScheduled" - - DataUpdateOptionalPowerConsumptionRunning api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionRunning" - - DataUpdateOptionalPowerConsumptionPaused api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionPaused" - - DataUpdateOptionalPowerConsumptionCompleted api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionCompleted" - DataUpdateOptionalPowerConsumptionStopped api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionStopped" DataUpdateMinimalRunDuration api.EventType = "cem-ohpcf-DataUpdateMinimalRunDuration" diff --git a/usecases/cem/ohpcf/usecase.go b/usecases/cem/ohpcf/usecase.go index 8734795e..117d359d 100644 --- a/usecases/cem/ohpcf/usecase.go +++ b/usecases/cem/ohpcf/usecase.go @@ -36,9 +36,9 @@ func NewOHPCF(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEvent usecase := usecase.NewUseCaseBase( localEntity, model.UseCaseActorTypeCEM, - model.UseCaseNameTypeEVStateOfCharge, + model.UseCaseNameTypeOptimizationOfSelfConsumptionByHeatPumpCompressorFlexibility, "1.0.0", - "RC1", + "release", useCaseScenarios, eventCB, UseCaseSupportUpdate, From 143285bf09c928cb09b985015d07a1ff785d3b80 Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Fri, 28 Mar 2025 10:02:15 +0100 Subject: [PATCH 04/12] =?UTF-8?q?Namens=C3=A4nderungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- usecases/api/cem_ohpcf.go | 2 +- usecases/cem/ohpcf/events.go | 6 +++--- usecases/cem/ohpcf/public.go | 2 +- usecases/cem/ohpcf/public_test.go | 6 +++--- usecases/cem/ohpcf/types.go | 6 ++---- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/usecases/api/cem_ohpcf.go b/usecases/api/cem_ohpcf.go index 8b6009bb..a9ca2289 100644 --- a/usecases/api/cem_ohpcf.go +++ b/usecases/api/cem_ohpcf.go @@ -25,7 +25,7 @@ type CemOHPCFInterface interface { // The maximal value for the requested power estimate [OHPCF-011/2/1]. // // return the maximal value for the requested power estimate - RequestPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) + RequestedPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) // Indication whether the consumption may be stopped by the CEM [OHPCF-011/5]. // diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go index 3c8ec5e1..52971e57 100644 --- a/usecases/cem/ohpcf/events.go +++ b/usecases/cem/ohpcf/events.go @@ -47,9 +47,9 @@ func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayloa if value.Value != nil && value.ValueType != nil { if *value.ValueType == model.PowerTimeSlotValueTypeTypePower { - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateRequestedPowerEstimate) } else if *value.ValueType == model.PowerTimeSlotValueTypeTypePowerMax { - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMaxPower) + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateRequestedPowerMax) } } } @@ -94,6 +94,6 @@ func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayloa } else if len(data.Alternatives) == 0 { // [OHPCF-003], [OHPCF-006/2] - o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOptionalPowerConsumptionStopped) + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionState) } } diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index 6524ccae..8d4b09b0 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -45,7 +45,7 @@ func (o *OHPCF) RequestedPowerEstimate(entity spineapi.EntityRemoteInterface) (f // The max power value [OHPCF-011/2/2]. // // return the maximal power value -func (o *OHPCF) RequestPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) { +func (o *OHPCF) RequestedPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) { return o.powerOfType(entity, model.PowerTimeSlotValueTypeTypePowerMax) } diff --git a/usecases/cem/ohpcf/public_test.go b/usecases/cem/ohpcf/public_test.go index 47c192eb..b325117e 100644 --- a/usecases/cem/ohpcf/public_test.go +++ b/usecases/cem/ohpcf/public_test.go @@ -68,10 +68,10 @@ func (s *CemOhPCFSuite) Test_Power() { } func (s *CemOhPCFSuite) Test_MaxPower() { - _, err := s.sut.RequestPowerMax(s.mockRemoteEntity) + _, err := s.sut.RequestedPowerMax(s.mockRemoteEntity) assert.NotNil(s.T(), err) - _, err = s.sut.RequestPowerMax(s.monitoredEntity) + _, err = s.sut.RequestedPowerMax(s.monitoredEntity) assert.NotNil(s.T(), err) data := &model.SmartEnergyManagementPsDataType{ @@ -93,7 +93,7 @@ func (s *CemOhPCFSuite) Test_MaxPower() { _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) assert.Nil(s.T(), fErr) - available, err := s.sut.RequestPowerMax(s.monitoredEntity) + available, err := s.sut.RequestedPowerMax(s.monitoredEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 1006.0, available) } diff --git a/usecases/cem/ohpcf/types.go b/usecases/cem/ohpcf/types.go index 8fe8f427..6662af4b 100644 --- a/usecases/cem/ohpcf/types.go +++ b/usecases/cem/ohpcf/types.go @@ -7,9 +7,9 @@ const ( // Scenario 1 - DataUpdatePower api.EventType = "cem-ohpcf-DataUpdatePower" + DataUpdateRequestedPowerEstimate api.EventType = "cem-ohpcf-DataUpdateRequestedPowerEstimate" - DataUpdateMaxPower api.EventType = "cem-ohpcf-DataUpdateMaxPower" + DataUpdateRequestedPowerMax api.EventType = "cem-ohpcf-DataUpdateRequestedPowerMax" DataUpdateConsumptionIsStoppable api.EventType = "cem-ohpcf-DataUpdateConsumptionIsStoppable" @@ -19,8 +19,6 @@ const ( DataUpdateConsumptionState api.EventType = "cem-ohpcf-DataUpdateConsumptionState" - DataUpdateOptionalPowerConsumptionStopped api.EventType = "cem-ohpcf-DataUpdateOptionalPowerConsumptionStopped" - DataUpdateMinimalRunDuration api.EventType = "cem-ohpcf-DataUpdateMinimalRunDuration" DataUpdateMinimalPauseDuration api.EventType = "cem-ohpcf-DataUpdateMinimalPauseDuration" From a7814af840c2d52992b7fa2bf26451961e50bec3 Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Mon, 31 Mar 2025 09:56:17 +0200 Subject: [PATCH 05/12] bugfixes: - subscribing to events is necessary --- examples/remote/main.go | 8 ++++++++ usecases/cem/ohpcf/events.go | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/examples/remote/main.go b/examples/remote/main.go index 611baaff..687ba5b9 100644 --- a/examples/remote/main.go +++ b/examples/remote/main.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "flag" + "github.com/enbility/eebus-go/usecases/cem/ohpcf" "log" "net" "os" @@ -139,6 +140,13 @@ func main() { log.Fatal(err) } + err = r.RegisterUseCase(model.EntityTypeTypeCEM, "CEM-OHPCF", func(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) api.UseCaseInterface { + return ohpcf.NewOHPCF(localEntity, eventCB) + }) + if err != nil { + log.Fatal(err) + } + ctx, cancelCtx := context.WithCancel(context.Background()) if err = r.Listen(ctx, "tcp", net.JoinHostPort("::", strconv.Itoa(3393))); err != nil { log.Fatal(err) diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go index 52971e57..5a1ea7b2 100644 --- a/usecases/cem/ohpcf/events.go +++ b/usecases/cem/ohpcf/events.go @@ -1,6 +1,7 @@ package ohpcf import ( + "github.com/enbility/eebus-go/usecases/internal" spineapi "github.com/enbility/spine-go/api" "github.com/enbility/spine-go/model" ) @@ -13,6 +14,12 @@ func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { return } + if internal.IsEntityAdded(payload) { + localFeature := o.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeClient) + remoteFeature := payload.Entity.FeatureOfTypeAndRole(model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + localFeature.SubscribeToRemote(remoteFeature.Address()) + } + if payload.Data == nil { return } From d78f3d0d16dee549f083e78ea67256c060eb0308 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Wed, 22 Oct 2025 13:50:56 +0200 Subject: [PATCH 06/12] Ensure CEM OHPCF creates binding upon connection with remote device. Also added function to get overview of currently announced optional power consumption on remote device --- usecases/api/types.go | 10 ++++ usecases/cem/ohpcf/events.go | 22 ++++++-- usecases/cem/ohpcf/public.go | 99 ++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/usecases/api/types.go b/usecases/api/types.go index 50c6c8b7..6defb831 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -17,6 +17,16 @@ const ( EVChargeStateTypeFinished EVChargeStateType = "finished" ) +type OptionalPowerConsumptionInfo struct { + PowerSequenceId model.PowerSequenceIdType + Power *float64 + MaxPower *float64 + State model.PowerSequenceStateType + IsPausable bool + IsStoppable bool + StartTime *time.Time +} + // manufacturer data type type ManufacturerData struct { DeviceName string diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go index 5a1ea7b2..8025be3e 100644 --- a/usecases/cem/ohpcf/events.go +++ b/usecases/cem/ohpcf/events.go @@ -1,7 +1,9 @@ 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" "github.com/enbility/spine-go/model" ) @@ -15,9 +17,7 @@ func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { } if internal.IsEntityAdded(payload) { - localFeature := o.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeClient) - remoteFeature := payload.Entity.FeatureOfTypeAndRole(model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) - localFeature.SubscribeToRemote(remoteFeature.Address()) + o.connected(payload.Entity) } if payload.Data == nil { @@ -31,6 +31,22 @@ func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { } } +func (o *OHPCF) connected(entity spineapi.EntityRemoteInterface) { + if semp, err := client.NewSmartEnergyManagementPs(o.LocalEntity, entity); err == nil { + if !semp.HasSubscription() { + if _, err := semp.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } + + if !semp.HasBinding() { + if _, err := semp.Bind(); err != nil { + logging.Log().Debug(err) + } + } + } +} + func (o *OHPCF) loadSmartEnergyManagementPsDataType(payload spineapi.EventPayload) { data := payload.Data.(*model.SmartEnergyManagementPsDataType) diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index 8d4b09b0..08d2531b 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -1,6 +1,8 @@ package ohpcf import ( + "fmt" + "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/features/client" ucapi "github.com/enbility/eebus-go/usecases/api" @@ -13,6 +15,95 @@ import ( // Scenario 1 +func (o *OHPCF) OptionalPowerConsumption(entity spineapi.EntityRemoteInterface) (*ucapi.OptionalPowerConsumptionInfo, error) { + data, err := o.checkEntityTypeAndGetData(entity) + if err != nil { + return nil, err + } + + if !o.isDataAvailable(data) { + return nil, nil + } + + alt := data.Alternatives + if len(alt) == 0 { + return nil, api.ErrDataNotAvailable + } + remoteDataStructureError := "The remote data structure is in an invalid state " + if len(alt[0].PowerSequence) == 0 { + return nil, fmt.Errorf("%s(alternative attribute present but no power sequence defined)", remoteDataStructureError) + } + + seq := alt[0].PowerSequence[0] + if seq.Description == nil || seq.Description.SequenceId == nil { + return nil, fmt.Errorf("%s(alternative attribute present but no power sequenceId defined)", remoteDataStructureError) + } + seqId := *seq.Description.SequenceId + if seq.State == nil || seq.State.State == nil { + return nil, fmt.Errorf("%s(alternative attribute present but no power sequence state defined)", remoteDataStructureError) + } + + if seq.OperatingConstraintsInterrupt == nil { + return nil, fmt.Errorf("%s(alternative attribute present but no power sequence operating interrupt constraints defined)", remoteDataStructureError) + } + isPausable := false + isStoppable := false + if seq.OperatingConstraintsInterrupt.IsPausable != nil { + isPausable = *seq.OperatingConstraintsInterrupt.IsPausable + } + if seq.OperatingConstraintsInterrupt.IsStoppable != nil { + isStoppable = *seq.OperatingConstraintsInterrupt.IsStoppable + } + + var startTime *time.Time + if seq.Schedule != nil && seq.Schedule.StartTime != nil { + parsedTime, err := seq.Schedule.StartTime.GetTime() + if err != nil { + logging.Log().Infof("CEM OHPCF: could not parse scheduled start time: %s", err.Error()) + } else { + startTime = &parsedTime + } + } + + slot := seq.PowerTimeSlot + if len(slot) == 0 || slot[0].ValueList == nil || len(slot[0].ValueList.Value) == 0 { + return nil, fmt.Errorf("%s(alternative and power sequence attributes present but no time slot defined or no values set for time slot)", remoteDataStructureError) + } + + var power *float64 + var maxPower *float64 + for _, value := range slot[0].ValueList.Value { + if value.ValueType == nil { + continue + } + + switch *value.ValueType { + case model.PowerTimeSlotValueTypeTypePower: + power = util.Ptr(value.Value.GetValue()) + case model.PowerTimeSlotValueTypeTypePowerMax: + maxPower = util.Ptr(value.Value.GetValue()) + default: + continue + } + } + if power == nil && maxPower == nil { + return nil, fmt.Errorf("%s(no power value with correct type (power or powerMax) is set)", remoteDataStructureError) + } + + info := ucapi.OptionalPowerConsumptionInfo{ + PowerSequenceId: seqId, + Power: power, + MaxPower: maxPower, + State: *seq.State.State, // safe to dereference because we validate above + IsPausable: isPausable, + IsStoppable: isStoppable, + StartTime: startTime, + } + + return &info, nil +} + + // The availability of an optional consumption of power [OHPCF-011/1]. // // return true if the optional consumption of power is available @@ -202,9 +293,17 @@ func (o *OHPCF) PowerConsumptionMinimalPauseDuration(entity spineapi.EntityRemot // parameters: // - start: The start time of the power consumption func (o *OHPCF) SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, start time.Time, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + info, err := o.OptionalPowerConsumption(entity) + if err != nil { + return nil, err + } + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: &info.PowerSequenceId, + }, Schedule: &model.PowerSequenceScheduleDataType{ StartTime: model.NewAbsoluteOrRelativeTimeTypeFromTime(start), }, From 03fd8d0f4bf61d220a9a851bd4b9f6e150939c04 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Wed, 22 Oct 2025 16:29:22 +0200 Subject: [PATCH 07/12] Send sequenceId when requesting state changes (mandatory per spec) --- usecases/cem/ohpcf/public.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index 08d2531b..b7384abb 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -316,9 +316,17 @@ func (o *OHPCF) SchedulePowerConsumptionProcess(entity spineapi.EntityRemoteInte // stop (abort) the process [OHPCF-022/1]. func (o *OHPCF) AbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + info, err := o.OptionalPowerConsumption(entity) + if err != nil { + return nil, err + } + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(info.PowerSequenceId), + }, State: &model.PowerSequenceStateDataType{ State: util.Ptr(model.PowerSequenceStateTypeInvalid), }, @@ -331,9 +339,17 @@ func (o *OHPCF) AbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterfa // pause the process [OHPCF-022/2]. func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + info, err := o.OptionalPowerConsumption(entity) + if err != nil { + return nil, err + } + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(info.PowerSequenceId), + }, State: &model.PowerSequenceStateDataType{ State: util.Ptr(model.PowerSequenceStateTypeScheduledPaused), }, @@ -346,9 +362,17 @@ func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterfa // resume the process [OHPCF-022/3]. func (o *OHPCF) ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) { + info, err := o.OptionalPowerConsumption(entity) + if err != nil { + return nil, err + } + data := &model.SmartEnergyManagementPsDataType{ Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(info.PowerSequenceId), + }, State: &model.PowerSequenceStateDataType{ State: util.Ptr(model.PowerSequenceStateTypeRunning), }, From cbb65df6a208d71e37db5b16081d2cd9e7fff760 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Wed, 22 Oct 2025 16:41:34 +0200 Subject: [PATCH 08/12] send correct state when pausing consumption process --- usecases/cem/ohpcf/public.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index b7384abb..a4a87f96 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -351,7 +351,7 @@ func (o *OHPCF) PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterfa SequenceId: util.Ptr(info.PowerSequenceId), }, State: &model.PowerSequenceStateDataType{ - State: util.Ptr(model.PowerSequenceStateTypeScheduledPaused), + State: util.Ptr(model.PowerSequenceStateTypePaused), }, }}, }}, From b8fc9742428a28105734851becba1622c8889ca5 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Tue, 28 Oct 2025 16:48:40 +0100 Subject: [PATCH 09/12] fixed error return --- usecases/cem/ohpcf/public.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usecases/cem/ohpcf/public.go b/usecases/cem/ohpcf/public.go index a4a87f96..781fcd40 100644 --- a/usecases/cem/ohpcf/public.go +++ b/usecases/cem/ohpcf/public.go @@ -22,7 +22,7 @@ func (o *OHPCF) OptionalPowerConsumption(entity spineapi.EntityRemoteInterface) } if !o.isDataAvailable(data) { - return nil, nil + return nil, api.ErrDataNotAvailable } alt := data.Alternatives From 46a86e6da3c89d2ee4de2dddf03e337e47756854 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Sat, 1 Nov 2025 15:59:48 +0100 Subject: [PATCH 10/12] Added OptionalPowerConsumption method to interface of CEM OHPCF --- usecases/api/cem_ohpcf.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/usecases/api/cem_ohpcf.go b/usecases/api/cem_ohpcf.go index a9ca2289..2dbde8e0 100644 --- a/usecases/api/cem_ohpcf.go +++ b/usecases/api/cem_ohpcf.go @@ -12,6 +12,11 @@ type CemOHPCFInterface interface { // Scenario 1 + // Get all relevant info for an available power consumption + // + // return OptionalPowerConsumptionInfo struct with all available info if an optional power consumption has been announced + OptionalPowerConsumption(entity spineapi.EntityRemoteInterface) (*OptionalPowerConsumptionInfo, error) + // The availability of an optional consumption of power [OHPCF-011/1]. // // return true if the optional consumption of power is possible From 00c54db39a3c0990a7ecece57c02267534def90d Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Mon, 8 Jun 2026 10:43:51 +0200 Subject: [PATCH 11/12] fix merge and implement tests --- usecases/cem/ohpcf/public_test.go | 139 +++++++++++++++++++++++++- usecases/cem/ohpcf/testhelper_test.go | 9 +- usecases/cem/ohpcf/usecase.go | 3 - 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/usecases/cem/ohpcf/public_test.go b/usecases/cem/ohpcf/public_test.go index b325117e..2f988a9f 100644 --- a/usecases/cem/ohpcf/public_test.go +++ b/usecases/cem/ohpcf/public_test.go @@ -268,7 +268,42 @@ func (s *CemOhPCFSuite) Test_SchedulePowerConsumptionProcess() { _, err := s.sut.SchedulePowerConsumptionProcess(s.mockRemoteEntity, time.Now(), nil) assert.NotNil(s.T(), err) - msgCounter, err := s.sut.SchedulePowerConsumptionProcess(s.monitoredEntity, time.Now(), nil) + // Without valid data, the call should fail + _, err = s.sut.SchedulePowerConsumptionProcess(s.monitoredEntity, time.Now(), nil) + assert.NotNil(s.T(), err) + + // Set up valid SmartEnergyManagementPs data + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(model.PowerSequenceIdType(1)), + }, + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeInactive), + }, + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsPausable: util.Ptr(true), + IsStoppable: util.Ptr(true), + }, + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1000), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePower), + }}, + }, + }}, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + + startTime := time.Now().Add(time.Hour) + msgCounter, err := s.sut.SchedulePowerConsumptionProcess(s.monitoredEntity, startTime, nil) assert.NotNil(s.T(), msgCounter) assert.Nil(s.T(), err) } @@ -277,6 +312,40 @@ func (s *CemOhPCFSuite) Test_StopAbortPowerConsumptionProcess() { _, err := s.sut.AbortPowerConsumptionProcess(s.mockRemoteEntity, nil) assert.NotNil(s.T(), err) + // Without valid data, the call should fail + _, err = s.sut.AbortPowerConsumptionProcess(s.monitoredEntity, nil) + assert.NotNil(s.T(), err) + + // Set up valid SmartEnergyManagementPs data + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(model.PowerSequenceIdType(1)), + }, + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeRunning), + }, + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsPausable: util.Ptr(true), + IsStoppable: util.Ptr(true), + }, + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1000), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePower), + }}, + }, + }}, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + msgCounter, err := s.sut.AbortPowerConsumptionProcess(s.monitoredEntity, nil) assert.NotNil(s.T(), msgCounter) assert.Nil(s.T(), err) @@ -286,6 +355,40 @@ func (s *CemOhPCFSuite) Test_PausePowerConsumptionProcess() { _, err := s.sut.PausePowerConsumptionProcess(s.mockRemoteEntity, nil) assert.NotNil(s.T(), err) + // Without valid data, the call should fail + _, err = s.sut.PausePowerConsumptionProcess(s.monitoredEntity, nil) + assert.NotNil(s.T(), err) + + // Set up valid SmartEnergyManagementPs data + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(model.PowerSequenceIdType(1)), + }, + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeRunning), + }, + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsPausable: util.Ptr(true), + IsStoppable: util.Ptr(true), + }, + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1000), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePower), + }}, + }, + }}, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + msgCounter, err := s.sut.PausePowerConsumptionProcess(s.monitoredEntity, nil) assert.NotNil(s.T(), msgCounter) assert.Nil(s.T(), err) @@ -295,6 +398,40 @@ func (s *CemOhPCFSuite) Test_ResumePowerConsumptionProcess() { _, err := s.sut.ResumePowerConsumptionProcess(s.mockRemoteEntity, nil) assert.NotNil(s.T(), err) + // Without valid data, the call should fail + _, err = s.sut.ResumePowerConsumptionProcess(s.monitoredEntity, nil) + assert.NotNil(s.T(), err) + + // Set up valid SmartEnergyManagementPs data with paused state + data := &model.SmartEnergyManagementPsDataType{ + Alternatives: []model.SmartEnergyManagementPsAlternativesType{{ + PowerSequence: []model.SmartEnergyManagementPsPowerSequenceType{{ + Description: &model.PowerSequenceDescriptionDataType{ + SequenceId: util.Ptr(model.PowerSequenceIdType(1)), + }, + State: &model.PowerSequenceStateDataType{ + State: util.Ptr(model.PowerSequenceStateTypeScheduledPaused), + }, + OperatingConstraintsInterrupt: &model.OperatingConstraintsInterruptDataType{ + IsPausable: util.Ptr(true), + IsStoppable: util.Ptr(true), + }, + PowerTimeSlot: []model.SmartEnergyManagementPsPowerTimeSlotType{{ + ValueList: &model.SmartEnergyManagementPsPowerTimeSlotValueListType{ + Value: []model.PowerTimeSlotValueDataType{{ + Value: model.NewScaledNumberType(1000), + ValueType: util.Ptr(model.PowerTimeSlotValueTypeTypePower), + }}, + }, + }}, + }}, + }}, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeSmartEnergyManagementPs, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSmartEnergyManagementPsData, data, nil, nil) + assert.Nil(s.T(), fErr) + msgCounter, err := s.sut.ResumePowerConsumptionProcess(s.monitoredEntity, nil) assert.NotNil(s.T(), msgCounter) assert.Nil(s.T(), err) diff --git a/usecases/cem/ohpcf/testhelper_test.go b/usecases/cem/ohpcf/testhelper_test.go index 4db30275..f17d2ef8 100644 --- a/usecases/cem/ohpcf/testhelper_test.go +++ b/usecases/cem/ohpcf/testhelper_test.go @@ -2,6 +2,9 @@ package ohpcf import ( "fmt" + "testing" + "time" + "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/mocks" "github.com/enbility/eebus-go/service" @@ -15,8 +18,6 @@ import ( "github.com/enbility/spine-go/util" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" - "testing" - "time" ) func TestCemOHPCFSuite(t *testing.T) { @@ -49,7 +50,7 @@ func (s *CemOhPCFSuite) BeforeTest(suiteName, testName string) { []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, model.DeviceTypeTypeEnergyManagementSystem, []model.EntityTypeType{model.EntityTypeTypeCEM}, - 9999, cert, time.Second*4) + 9999, cert, time.Second*4, nil, nil) serviceHandler := mocks.NewServiceReaderInterface(s.T()) serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() @@ -155,7 +156,7 @@ func setupDevices( FeatureInformation: featureInformations, } - entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData, nil) if err != nil { fmt.Println(err) } diff --git a/usecases/cem/ohpcf/usecase.go b/usecases/cem/ohpcf/usecase.go index 117d359d..c40284b2 100644 --- a/usecases/cem/ohpcf/usecase.go +++ b/usecases/cem/ohpcf/usecase.go @@ -6,7 +6,6 @@ import ( "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 OHPCF struct { @@ -50,8 +49,6 @@ func NewOHPCF(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEvent UseCaseBase: usecase, } - _ = spine.Events.Subscribe(uc) - return uc } From abdcf864d0cfbd9b3d0eb4d7270674a954f520a9 Mon Sep 17 00:00:00 2001 From: Nils Prommersberger Date: Mon, 8 Jun 2026 12:59:36 +0200 Subject: [PATCH 12/12] remove break in events.go --- usecases/cem/ohpcf/events.go | 1 - 1 file changed, 1 deletion(-) diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go index 8025be3e..ebc1656e 100644 --- a/usecases/cem/ohpcf/events.go +++ b/usecases/cem/ohpcf/events.go @@ -27,7 +27,6 @@ func (o *OHPCF) HandleEvent(payload spineapi.EventPayload) { switch payload.Data.(type) { case *model.SmartEnergyManagementPsDataType: o.loadSmartEnergyManagementPsDataType(payload) - break } }