diff --git a/examples/remote/main.go b/examples/remote/main.go index c2e445b8..a7b366bc 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/api/cem_ohpcf.go b/usecases/api/cem_ohpcf.go new file mode 100644 index 00000000..2dbde8e0 --- /dev/null +++ b/usecases/api/cem_ohpcf.go @@ -0,0 +1,85 @@ +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 + + // 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 + OptionalPowerConsumptionAvailable(entity spineapi.EntityRemoteInterface) (bool, error) + + // The requested power estimate value [OHPCF-011/2/1]. + // + // 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 + RequestedPowerMax(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: + // 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, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) + + // stop (abort) the process [OHPCF-022/1]. + AbortPowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) + + // pause the process [OHPCF-022/2]. + PausePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) + + // resume the process [OHPCF-022/3]. + ResumePowerConsumptionProcess(entity spineapi.EntityRemoteInterface, resultCB func(result model.ResultDataType)) (*model.MsgCounterType, error) +} diff --git a/usecases/api/types.go b/usecases/api/types.go index ecb3d016..dc125fad 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 @@ -167,6 +177,17 @@ type DurationSlotValue struct { 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) +) + type PendingDeviceConfiguration struct { Description model.DeviceConfigurationKeyValueDescriptionDataType `json:"description"` KeyName model.DeviceConfigurationKeyNameType `json:"keyName"` diff --git a/usecases/cem/ohpcf/events.go b/usecases/cem/ohpcf/events.go new file mode 100644 index 00000000..ebc1656e --- /dev/null +++ b/usecases/cem/ohpcf/events.go @@ -0,0 +1,121 @@ +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" +) + +// 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 internal.IsEntityAdded(payload) { + o.connected(payload.Entity) + } + + if payload.Data == nil { + return + } + + switch payload.Data.(type) { + case *model.SmartEnergyManagementPsDataType: + o.loadSmartEnergyManagementPsDataType(payload) + } +} + +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) + + if o.EventCB == nil { + return + } + + if len(data.Alternatives) == 1 { + 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, DataUpdateRequestedPowerEstimate) + } else if *value.ValueType == model.PowerTimeSlotValueTypeTypePowerMax { + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateRequestedPowerMax) + } + } + } + } + + 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 request.OperatingConstraintsInterrupt.IsPausable != nil { + // [OHPCF-011/6] + // [OHPCF-012/3] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionIsPausable) + } + } + + if request.Schedule != nil && + request.Schedule.StartTime != nil { + // [OHPCF-012/1] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateConsumptionStartTime) + } + + 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 request.OperatingConstraintsDuration != nil { + if request.OperatingConstraintsDuration.ActiveDurationMin != nil { + // [OHPCF-008] + o.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateMinimalRunDuration) + } + 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, DataUpdateConsumptionState) + } +} diff --git a/usecases/cem/ohpcf/events_test.go b/usecases/cem/ohpcf/events_test.go new file mode 100644 index 00000000..3172bcb4 --- /dev/null +++ b/usecases/cem/ohpcf/events_test.go @@ -0,0 +1,183 @@ +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), + 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), + }}, + }, + }}, + }}, + }}, + } + 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..781fcd40 --- /dev/null +++ b/usecases/cem/ohpcf/public.go @@ -0,0 +1,466 @@ +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" + "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" + "time" +) + +// 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, api.ErrDataNotAvailable + } + + 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 +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, nil + } + + 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 || + *data.Alternatives[0].PowerSequence[0].State.State == model.PowerSequenceStateTypeScheduled, nil + } + + return false, nil +} + +// The power value [OHPCF-011/2/1]. +// +// return the power value +func (o *OHPCF) RequestedPowerEstimate(entity spineapi.EntityRemoteInterface) (float64, error) { + return o.powerOfType(entity, model.PowerTimeSlotValueTypeTypePower) +} + +// The max power value [OHPCF-011/2/2]. +// +// return the maximal power value +func (o *OHPCF) RequestedPowerMax(entity spineapi.EntityRemoteInterface) (float64, error) { + return o.powerOfType(entity, model.PowerTimeSlotValueTypeTypePowerMax) +} + +// 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 ucapi.CompressorPowerConsumptionStateStopped, nil + } + + 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, 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), + }, + }}, + }}, + } + + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} + +// 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), + }, + }}, + }}, + } + + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} + +// 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.PowerSequenceStateTypePaused), + }, + }}, + }}, + } + + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} + +// 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), + }, + }}, + }}, + } + + return o.writeSmartEnergyManagementData(entity, data, resultCB) +} + +// ------------------------ 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 +} + +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 new file mode 100644 index 00000000..2f988a9f --- /dev/null +++ b/usecases/cem/ohpcf/public_test.go @@ -0,0 +1,521 @@ +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" + "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.RequestedPowerEstimate(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.RequestedPowerEstimate(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), + 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) + + 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.RequestedPowerMax(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.RequestedPowerMax(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.RequestedPowerMax(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) + + _, 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(), api.CompressorPowerConsumptionStateAvailable, 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) + + // 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) +} + +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) +} + +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) +} + +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) +} + +// 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..f17d2ef8 --- /dev/null +++ b/usecases/cem/ohpcf/testhelper_test.go @@ -0,0 +1,172 @@ +package ohpcf + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestCemOHPCFSuite(t *testing.T) { + suite.Run(t, new(CemOhPCFSuite)) +} + +type CemOhPCFSuite struct { + suite.Suite + + sut *OHPCF + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *CemOhPCFSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *CemOhPCFSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, time.Second*4, nil, nil) + + 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, nil) + 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..6662af4b --- /dev/null +++ b/usecases/cem/ohpcf/types.go @@ -0,0 +1,25 @@ +package ohpcf + +import "github.com/enbility/eebus-go/api" + +const ( + UseCaseSupportUpdate api.EventType = "cem-ohpcf-UseCaseSupportUpdate" + + // Scenario 1 + + DataUpdateRequestedPowerEstimate api.EventType = "cem-ohpcf-DataUpdateRequestedPowerEstimate" + + DataUpdateRequestedPowerMax api.EventType = "cem-ohpcf-DataUpdateRequestedPowerMax" + + DataUpdateConsumptionIsStoppable api.EventType = "cem-ohpcf-DataUpdateConsumptionIsStoppable" + + DataUpdateConsumptionIsPausable api.EventType = "cem-ohpcf-DataUpdateConsumptionIsPausable" + + DataUpdateConsumptionStartTime api.EventType = "cem-ohpcf-DataUpdateConsumptionStartTime" + + DataUpdateConsumptionState api.EventType = "cem-ohpcf-DataUpdateConsumptionState" + + 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..c40284b2 --- /dev/null +++ b/usecases/cem/ohpcf/usecase.go @@ -0,0 +1,63 @@ +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" +) + +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.UseCaseNameTypeOptimizationOfSelfConsumptionByHeatPumpCompressorFlexibility, + "1.0.0", + "release", + useCaseScenarios, + eventCB, + UseCaseSupportUpdate, + validActorTypes, + validEntityTypes, + ) + + uc := &OHPCF{ + UseCaseBase: usecase, + } + + 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) +}