From 02b8f694d48caac691a2f4b4c5672179ecdc012b Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 16 Apr 2024 14:29:39 +0200 Subject: [PATCH 1/2] Add UCLPP and UCLPPServer Add implementations for the use case Limitation of Power Production (LPP) as client and server --- .mockery.yaml | 2 + mocks/UCLPPInterface.go | 640 ++++++++++++++++++++++++++++++++ mocks/UCLPPServerInterface.go | 659 +++++++++++++++++++++++++++++++++ uclpp/api.go | 87 +++++ uclpp/events.go | 114 ++++++ uclpp/events_test.go | 84 +++++ uclpp/public.go | 304 +++++++++++++++ uclpp/public_test.go | 355 ++++++++++++++++++ uclpp/testhelper_test.go | 182 +++++++++ uclpp/types.go | 38 ++ uclpp/uclpp.go | 119 ++++++ uclpp/uclpp_test.go | 45 +++ uclppserver/api.go | 80 ++++ uclppserver/events.go | 152 ++++++++ uclppserver/events_test.go | 204 ++++++++++ uclppserver/public.go | 206 +++++++++++ uclppserver/public_test.go | 77 ++++ uclppserver/testhelper_test.go | 197 ++++++++++ uclppserver/types.go | 38 ++ uclppserver/uclpp.go | 208 +++++++++++ uclppserver/uclpp_test.go | 45 +++ 21 files changed, 3836 insertions(+) create mode 100644 mocks/UCLPPInterface.go create mode 100644 mocks/UCLPPServerInterface.go create mode 100644 uclpp/api.go create mode 100644 uclpp/events.go create mode 100644 uclpp/events_test.go create mode 100644 uclpp/public.go create mode 100644 uclpp/public_test.go create mode 100644 uclpp/testhelper_test.go create mode 100644 uclpp/types.go create mode 100644 uclpp/uclpp.go create mode 100644 uclpp/uclpp_test.go create mode 100644 uclppserver/api.go create mode 100644 uclppserver/events.go create mode 100644 uclppserver/events_test.go create mode 100644 uclppserver/public.go create mode 100644 uclppserver/public_test.go create mode 100644 uclppserver/testhelper_test.go create mode 100644 uclppserver/types.go create mode 100644 uclppserver/uclpp.go create mode 100644 uclppserver/uclpp_test.go diff --git a/.mockery.yaml b/.mockery.yaml index 33fe715..29eacfc 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -15,6 +15,8 @@ packages: github.com/enbility/cemd/ucevsoc: github.com/enbility/cemd/uclpc: github.com/enbility/cemd/uclpcserver: + github.com/enbility/cemd/uclpp: + github.com/enbility/cemd/uclppserver: github.com/enbility/cemd/ucmgcp: github.com/enbility/cemd/ucmpc: github.com/enbility/cemd/ucopev: diff --git a/mocks/UCLPPInterface.go b/mocks/UCLPPInterface.go new file mode 100644 index 0000000..fa99ced --- /dev/null +++ b/mocks/UCLPPInterface.go @@ -0,0 +1,640 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + time "time" +) + +// UCLPPInterface is an autogenerated mock type for the UCLPPInterface type +type UCLPPInterface struct { + mock.Mock +} + +type UCLPPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCLPPInterface) EXPECT() *UCLPPInterface_Expecter { + return &UCLPPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCLPPInterface) AddFeatures() { + _m.Called() +} + +// UCLPPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCLPPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCLPPInterface_Expecter) AddFeatures() *UCLPPInterface_AddFeatures_Call { + return &UCLPPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCLPPInterface_AddFeatures_Call) Run(run func()) *UCLPPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPInterface_AddFeatures_Call) Return() *UCLPPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPInterface_AddFeatures_Call) RunAndReturn(run func()) *UCLPPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCLPPInterface) AddUseCase() { + _m.Called() +} + +// UCLPPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCLPPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCLPPInterface_Expecter) AddUseCase() *UCLPPInterface_AddUseCase_Call { + return &UCLPPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCLPPInterface_AddUseCase_Call) Run(run func()) *UCLPPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPInterface_AddUseCase_Call) Return() *UCLPPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPInterface_AddUseCase_Call) RunAndReturn(run func()) *UCLPPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: entity +func (_m *UCLPPInterface) FailsafeDurationMinimum(entity api.EntityRemoteInterface) (time.Duration, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeDurationMinimum") + } + + var r0 time.Duration + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (time.Duration, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) time.Duration); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type UCLPPInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) FailsafeDurationMinimum(entity interface{}) *UCLPPInterface_FailsafeDurationMinimum_Call { + return &UCLPPInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum", entity)} +} + +func (_c *UCLPPInterface_FailsafeDurationMinimum_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_FailsafeDurationMinimum_Call) Return(_a0 time.Duration, _a1 error) *UCLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func(api.EntityRemoteInterface) (time.Duration, error)) *UCLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeProductionActivePowerLimit provides a mock function with given fields: entity +func (_m *UCLPPInterface) FailsafeProductionActivePowerLimit(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeProductionActivePowerLimit") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_FailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeProductionActivePowerLimit' +type UCLPPInterface_FailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) FailsafeProductionActivePowerLimit(entity interface{}) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + return &UCLPPInterface_FailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("FailsafeProductionActivePowerLimit", entity)} +} + +func (_c *UCLPPInterface_FailsafeProductionActivePowerLimit_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_FailsafeProductionActivePowerLimit_Call) Return(_a0 float64, _a1 error) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_FailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCLPPInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCLPPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCLPPInterface_IsUseCaseSupported_Call { + return &UCLPPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCLPPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCLPPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PowerProductionNominalMax provides a mock function with given fields: entity +func (_m *UCLPPInterface) PowerProductionNominalMax(entity api.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerProductionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_PowerProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerProductionNominalMax' +type UCLPPInterface_PowerProductionNominalMax_Call struct { + *mock.Call +} + +// PowerProductionNominalMax is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) PowerProductionNominalMax(entity interface{}) *UCLPPInterface_PowerProductionNominalMax_Call { + return &UCLPPInterface_PowerProductionNominalMax_Call{Call: _e.mock.On("PowerProductionNominalMax", entity)} +} + +func (_c *UCLPPInterface_PowerProductionNominalMax_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_PowerProductionNominalMax_Call) Return(_a0 float64, _a1 error) *UCLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_PowerProductionNominalMax_Call) RunAndReturn(run func(api.EntityRemoteInterface) (float64, error)) *UCLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// ProductionLimit provides a mock function with given fields: entity +func (_m *UCLPPInterface) ProductionLimit(entity api.EntityRemoteInterface) (cemdapi.LoadLimit, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ProductionLimit") + } + + var r0 cemdapi.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (cemdapi.LoadLimit, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) cemdapi.LoadLimit); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(cemdapi.LoadLimit) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_ProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProductionLimit' +type UCLPPInterface_ProductionLimit_Call struct { + *mock.Call +} + +// ProductionLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *UCLPPInterface_Expecter) ProductionLimit(entity interface{}) *UCLPPInterface_ProductionLimit_Call { + return &UCLPPInterface_ProductionLimit_Call{Call: _e.mock.On("ProductionLimit", entity)} +} + +func (_c *UCLPPInterface_ProductionLimit_Call) Run(run func(entity api.EntityRemoteInterface)) *UCLPPInterface_ProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPInterface_ProductionLimit_Call) Return(limit cemdapi.LoadLimit, resultErr error) *UCLPPInterface_ProductionLimit_Call { + _c.Call.Return(limit, resultErr) + return _c +} + +func (_c *UCLPPInterface_ProductionLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface) (cemdapi.LoadLimit, error)) *UCLPPInterface_ProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCLPPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCLPPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCLPPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCLPPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCLPPInterface_UpdateUseCaseAvailability_Call { + return &UCLPPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCLPPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCLPPInterface_UpdateUseCaseAvailability_Call) Return() *UCLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCLPPInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCLPPInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCLPPInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCLPPInterface_Expecter) UseCaseName() *UCLPPInterface_UseCaseName_Call { + return &UCLPPInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCLPPInterface_UseCaseName_Call) Run(run func()) *UCLPPInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCLPPInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCLPPInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCLPPInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeDurationMinimum provides a mock function with given fields: entity, duration +func (_m *UCLPPInterface) WriteFailsafeDurationMinimum(entity api.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + ret := _m.Called(entity, duration) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeDurationMinimum") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)); ok { + return rf(entity, duration) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, time.Duration) *model.MsgCounterType); ok { + r0 = rf(entity, duration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, time.Duration) error); ok { + r1 = rf(entity, duration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_WriteFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeDurationMinimum' +type UCLPPInterface_WriteFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// WriteFailsafeDurationMinimum is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - duration time.Duration +func (_e *UCLPPInterface_Expecter) WriteFailsafeDurationMinimum(entity interface{}, duration interface{}) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + return &UCLPPInterface_WriteFailsafeDurationMinimum_Call{Call: _e.mock.On("WriteFailsafeDurationMinimum", entity, duration)} +} + +func (_c *UCLPPInterface_WriteFailsafeDurationMinimum_Call) Run(run func(entity api.EntityRemoteInterface, duration time.Duration)) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(time.Duration)) + }) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeDurationMinimum_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeDurationMinimum_Call) RunAndReturn(run func(api.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)) *UCLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeProductionActivePowerLimit provides a mock function with given fields: entity, value +func (_m *UCLPPInterface) WriteFailsafeProductionActivePowerLimit(entity api.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + ret := _m.Called(entity, value) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeProductionActivePowerLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, float64) (*model.MsgCounterType, error)); ok { + return rf(entity, value) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, float64) *model.MsgCounterType); ok { + r0 = rf(entity, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, float64) error); ok { + r1 = rf(entity, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeProductionActivePowerLimit' +type UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// WriteFailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - value float64 +func (_e *UCLPPInterface_Expecter) WriteFailsafeProductionActivePowerLimit(entity interface{}, value interface{}) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + return &UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("WriteFailsafeProductionActivePowerLimit", entity, value)} +} + +func (_c *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) Run(run func(entity api.EntityRemoteInterface, value float64)) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(float64)) + }) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface, float64) (*model.MsgCounterType, error)) *UCLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteProductionLimit provides a mock function with given fields: entity, limit +func (_m *UCLPPInterface) WriteProductionLimit(entity api.EntityRemoteInterface, limit cemdapi.LoadLimit) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limit) + + if len(ret) == 0 { + panic("no return value specified for WriteProductionLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) (*model.MsgCounterType, error)); ok { + return rf(entity, limit) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) *model.MsgCounterType); ok { + r0 = rf(entity, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface, cemdapi.LoadLimit) error); ok { + r1 = rf(entity, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPInterface_WriteProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteProductionLimit' +type UCLPPInterface_WriteProductionLimit_Call struct { + *mock.Call +} + +// WriteProductionLimit is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - limit cemdapi.LoadLimit +func (_e *UCLPPInterface_Expecter) WriteProductionLimit(entity interface{}, limit interface{}) *UCLPPInterface_WriteProductionLimit_Call { + return &UCLPPInterface_WriteProductionLimit_Call{Call: _e.mock.On("WriteProductionLimit", entity, limit)} +} + +func (_c *UCLPPInterface_WriteProductionLimit_Call) Run(run func(entity api.EntityRemoteInterface, limit cemdapi.LoadLimit)) *UCLPPInterface_WriteProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface), args[1].(cemdapi.LoadLimit)) + }) + return _c +} + +func (_c *UCLPPInterface_WriteProductionLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *UCLPPInterface_WriteProductionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPInterface_WriteProductionLimit_Call) RunAndReturn(run func(api.EntityRemoteInterface, cemdapi.LoadLimit) (*model.MsgCounterType, error)) *UCLPPInterface_WriteProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// NewUCLPPInterface creates a new instance of UCLPPInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCLPPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCLPPInterface { + mock := &UCLPPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UCLPPServerInterface.go b/mocks/UCLPPServerInterface.go new file mode 100644 index 0000000..1d6273f --- /dev/null +++ b/mocks/UCLPPServerInterface.go @@ -0,0 +1,659 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + cemdapi "github.com/enbility/cemd/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + time "time" +) + +// UCLPPServerInterface is an autogenerated mock type for the UCLPPServerInterface type +type UCLPPServerInterface struct { + mock.Mock +} + +type UCLPPServerInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UCLPPServerInterface) EXPECT() *UCLPPServerInterface_Expecter { + return &UCLPPServerInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UCLPPServerInterface) AddFeatures() { + _m.Called() +} + +// UCLPPServerInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UCLPPServerInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) AddFeatures() *UCLPPServerInterface_AddFeatures_Call { + return &UCLPPServerInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UCLPPServerInterface_AddFeatures_Call) Run(run func()) *UCLPPServerInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_AddFeatures_Call) Return() *UCLPPServerInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPServerInterface_AddFeatures_Call) RunAndReturn(run func()) *UCLPPServerInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UCLPPServerInterface) AddUseCase() { + _m.Called() +} + +// UCLPPServerInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UCLPPServerInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) AddUseCase() *UCLPPServerInterface_AddUseCase_Call { + return &UCLPPServerInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UCLPPServerInterface_AddUseCase_Call) Run(run func()) *UCLPPServerInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_AddUseCase_Call) Return() *UCLPPServerInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPServerInterface_AddUseCase_Call) RunAndReturn(run func()) *UCLPPServerInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ContractualProductionNominalMax provides a mock function with given fields: +func (_m *UCLPPServerInterface) ContractualProductionNominalMax() (float64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ContractualProductionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func() (float64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPServerInterface_ContractualProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContractualProductionNominalMax' +type UCLPPServerInterface_ContractualProductionNominalMax_Call struct { + *mock.Call +} + +// ContractualProductionNominalMax is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) ContractualProductionNominalMax() *UCLPPServerInterface_ContractualProductionNominalMax_Call { + return &UCLPPServerInterface_ContractualProductionNominalMax_Call{Call: _e.mock.On("ContractualProductionNominalMax")} +} + +func (_c *UCLPPServerInterface_ContractualProductionNominalMax_Call) Run(run func()) *UCLPPServerInterface_ContractualProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_ContractualProductionNominalMax_Call) Return(_a0 float64, _a1 error) *UCLPPServerInterface_ContractualProductionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPServerInterface_ContractualProductionNominalMax_Call) RunAndReturn(run func() (float64, error)) *UCLPPServerInterface_ContractualProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: +func (_m *UCLPPServerInterface) FailsafeDurationMinimum() (time.Duration, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeDurationMinimum") + } + + var r0 time.Duration + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func() (time.Duration, bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCLPPServerInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type UCLPPServerInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) FailsafeDurationMinimum() *UCLPPServerInterface_FailsafeDurationMinimum_Call { + return &UCLPPServerInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum")} +} + +func (_c *UCLPPServerInterface_FailsafeDurationMinimum_Call) Run(run func()) *UCLPPServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeDurationMinimum_Call) Return(duration time.Duration, isChangeable bool, resultErr error) *UCLPPServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(duration, isChangeable, resultErr) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func() (time.Duration, bool, error)) *UCLPPServerInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeProductionActivePowerLimit provides a mock function with given fields: +func (_m *UCLPPServerInterface) FailsafeProductionActivePowerLimit() (float64, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeProductionActivePowerLimit") + } + + var r0 float64 + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func() (float64, bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() float64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func() error); ok { + r2 = rf() + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeProductionActivePowerLimit' +type UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeProductionActivePowerLimit is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) FailsafeProductionActivePowerLimit() *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + return &UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("FailsafeProductionActivePowerLimit")} +} + +func (_c *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call) Run(run func()) *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call) Return(value float64, isChangeable bool, resultErr error) *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(value, isChangeable, resultErr) + return _c +} + +func (_c *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call) RunAndReturn(run func() (float64, bool, error)) *UCLPPServerInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UCLPPServerInterface) IsUseCaseSupported(remoteEntity api.EntityRemoteInterface) (bool, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for IsUseCaseSupported") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPServerInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UCLPPServerInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity api.EntityRemoteInterface +func (_e *UCLPPServerInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UCLPPServerInterface_IsUseCaseSupported_Call { + return &UCLPPServerInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UCLPPServerInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity api.EntityRemoteInterface)) *UCLPPServerInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UCLPPServerInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UCLPPServerInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPServerInterface_IsUseCaseSupported_Call) RunAndReturn(run func(api.EntityRemoteInterface) (bool, error)) *UCLPPServerInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// ProductionLimit provides a mock function with given fields: +func (_m *UCLPPServerInterface) ProductionLimit() (cemdapi.LoadLimit, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ProductionLimit") + } + + var r0 cemdapi.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func() (cemdapi.LoadLimit, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() cemdapi.LoadLimit); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(cemdapi.LoadLimit) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UCLPPServerInterface_ProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProductionLimit' +type UCLPPServerInterface_ProductionLimit_Call struct { + *mock.Call +} + +// ProductionLimit is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) ProductionLimit() *UCLPPServerInterface_ProductionLimit_Call { + return &UCLPPServerInterface_ProductionLimit_Call{Call: _e.mock.On("ProductionLimit")} +} + +func (_c *UCLPPServerInterface_ProductionLimit_Call) Run(run func()) *UCLPPServerInterface_ProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_ProductionLimit_Call) Return(_a0 cemdapi.LoadLimit, _a1 error) *UCLPPServerInterface_ProductionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UCLPPServerInterface_ProductionLimit_Call) RunAndReturn(run func() (cemdapi.LoadLimit, error)) *UCLPPServerInterface_ProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetContractualProductionNominalMax provides a mock function with given fields: value +func (_m *UCLPPServerInterface) SetContractualProductionNominalMax(value float64) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for SetContractualProductionNominalMax") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetContractualProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetContractualProductionNominalMax' +type UCLPPServerInterface_SetContractualProductionNominalMax_Call struct { + *mock.Call +} + +// SetContractualProductionNominalMax is a helper method to define mock.On call +// - value float64 +func (_e *UCLPPServerInterface_Expecter) SetContractualProductionNominalMax(value interface{}) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + return &UCLPPServerInterface_SetContractualProductionNominalMax_Call{Call: _e.mock.On("SetContractualProductionNominalMax", value)} +} + +func (_c *UCLPPServerInterface_SetContractualProductionNominalMax_Call) Run(run func(value float64)) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetContractualProductionNominalMax_Call) Return(resultErr error) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetContractualProductionNominalMax_Call) RunAndReturn(run func(float64) error) *UCLPPServerInterface_SetContractualProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeDurationMinimum provides a mock function with given fields: duration, changeable +func (_m *UCLPPServerInterface) SetFailsafeDurationMinimum(duration time.Duration, changeable bool) error { + ret := _m.Called(duration, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeDurationMinimum") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Duration, bool) error); ok { + r0 = rf(duration, changeable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeDurationMinimum' +type UCLPPServerInterface_SetFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// SetFailsafeDurationMinimum is a helper method to define mock.On call +// - duration time.Duration +// - changeable bool +func (_e *UCLPPServerInterface_Expecter) SetFailsafeDurationMinimum(duration interface{}, changeable interface{}) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + return &UCLPPServerInterface_SetFailsafeDurationMinimum_Call{Call: _e.mock.On("SetFailsafeDurationMinimum", duration, changeable)} +} + +func (_c *UCLPPServerInterface_SetFailsafeDurationMinimum_Call) Run(run func(duration time.Duration, changeable bool)) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration), args[1].(bool)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeDurationMinimum_Call) Return(resultErr error) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeDurationMinimum_Call) RunAndReturn(run func(time.Duration, bool) error) *UCLPPServerInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeProductionActivePowerLimit provides a mock function with given fields: value, changeable +func (_m *UCLPPServerInterface) SetFailsafeProductionActivePowerLimit(value float64, changeable bool) error { + ret := _m.Called(value, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeProductionActivePowerLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64, bool) error); ok { + r0 = rf(value, changeable) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeProductionActivePowerLimit' +type UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// SetFailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - value float64 +// - changeable bool +func (_e *UCLPPServerInterface_Expecter) SetFailsafeProductionActivePowerLimit(value interface{}, changeable interface{}) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + return &UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("SetFailsafeProductionActivePowerLimit", value, changeable)} +} + +func (_c *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call) Run(run func(value float64, changeable bool)) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64), args[1].(bool)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call) Return(resultErr error) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(float64, bool) error) *UCLPPServerInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetProductionLimit provides a mock function with given fields: limit +func (_m *UCLPPServerInterface) SetProductionLimit(limit cemdapi.LoadLimit) error { + ret := _m.Called(limit) + + if len(ret) == 0 { + panic("no return value specified for SetProductionLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(cemdapi.LoadLimit) error); ok { + r0 = rf(limit) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UCLPPServerInterface_SetProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetProductionLimit' +type UCLPPServerInterface_SetProductionLimit_Call struct { + *mock.Call +} + +// SetProductionLimit is a helper method to define mock.On call +// - limit cemdapi.LoadLimit +func (_e *UCLPPServerInterface_Expecter) SetProductionLimit(limit interface{}) *UCLPPServerInterface_SetProductionLimit_Call { + return &UCLPPServerInterface_SetProductionLimit_Call{Call: _e.mock.On("SetProductionLimit", limit)} +} + +func (_c *UCLPPServerInterface_SetProductionLimit_Call) Run(run func(limit cemdapi.LoadLimit)) *UCLPPServerInterface_SetProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(cemdapi.LoadLimit)) + }) + return _c +} + +func (_c *UCLPPServerInterface_SetProductionLimit_Call) Return(resultErr error) *UCLPPServerInterface_SetProductionLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *UCLPPServerInterface_SetProductionLimit_Call) RunAndReturn(run func(cemdapi.LoadLimit) error) *UCLPPServerInterface_SetProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UCLPPServerInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UCLPPServerInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UCLPPServerInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UCLPPServerInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + return &UCLPPServerInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UCLPPServerInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UCLPPServerInterface_UpdateUseCaseAvailability_Call) Return() *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UCLPPServerInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UCLPPServerInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// UseCaseName provides a mock function with given fields: +func (_m *UCLPPServerInterface) UseCaseName() model.UseCaseNameType { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for UseCaseName") + } + + var r0 model.UseCaseNameType + if rf, ok := ret.Get(0).(func() model.UseCaseNameType); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(model.UseCaseNameType) + } + + return r0 +} + +// UCLPPServerInterface_UseCaseName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UseCaseName' +type UCLPPServerInterface_UseCaseName_Call struct { + *mock.Call +} + +// UseCaseName is a helper method to define mock.On call +func (_e *UCLPPServerInterface_Expecter) UseCaseName() *UCLPPServerInterface_UseCaseName_Call { + return &UCLPPServerInterface_UseCaseName_Call{Call: _e.mock.On("UseCaseName")} +} + +func (_c *UCLPPServerInterface_UseCaseName_Call) Run(run func()) *UCLPPServerInterface_UseCaseName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UCLPPServerInterface_UseCaseName_Call) Return(_a0 model.UseCaseNameType) *UCLPPServerInterface_UseCaseName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UCLPPServerInterface_UseCaseName_Call) RunAndReturn(run func() model.UseCaseNameType) *UCLPPServerInterface_UseCaseName_Call { + _c.Call.Return(run) + return _c +} + +// NewUCLPPServerInterface creates a new instance of UCLPPServerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewUCLPPServerInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UCLPPServerInterface { + mock := &UCLPPServerInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/uclpp/api.go b/uclpp/api.go new file mode 100644 index 0000000..53c64e9 --- /dev/null +++ b/uclpp/api.go @@ -0,0 +1,87 @@ +package uclpp + +import ( + "time" + + "github.com/enbility/cemd/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +//go:generate mockery + +// interface for the Limitation of Power Production UseCase +type UCLPPInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current production limit data + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ProductionLimit(entity spineapi.EntityRemoteInterface) (limit api.LoadLimit, resultErr error) + + // send new LoadControlLimits + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - limit: load limit data + WriteProductionLimit(entity spineapi.EntityRemoteInterface, limit api.LoadLimit) (*model.MsgCounterType, error) + + // Scenario 2 + + // return Failsafe limit for the produced active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - positive values are used for production + FailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) + + // send new Failsafe Production Active Power Limit + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - value: the new limit in W + WriteFailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) + + // return minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // parameters: + // - entity: the entity of the e.g. EVSE + // + // return values: + // - negative values are used for production + FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) + + // send new Failsafe Duration Minimum + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - duration: the duration, between 2h and 24h + WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) + + // Scenario 3 + + // this is automatically covered by the SPINE implementation + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // able to produce according to the device label or data sheet. + // + // parameters: + // - entity: the entity of the e.g. EVSE + PowerProductionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/uclpp/events.go b/uclpp/events.go new file mode 100644 index 0000000..fd81811 --- /dev/null +++ b/uclpp/events.go @@ -0,0 +1,114 @@ +package uclpp + +import ( + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCLPP) HandleEvent(payload spineapi.EventPayload) { + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + if util.IsEntityConnected(payload) { + e.connected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.LoadControlLimitDescriptionListDataType: + e.loadControlLimitDescriptionDataUpdate(payload.Entity) + case *model.LoadControlLimitListDataType: + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.configurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.configurationDataUpdate(payload) + } +} + +// the remote entity was connected +func (e *UCLPP) connected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if loadControl, err := util.LoadControl(e.service, entity); err == nil { + if _, err := loadControl.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get descriptions + if _, err := loadControl.RequestLimitDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data was updated +func (e *UCLPP) loadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if loadControl, err := util.LoadControl(e.service, entity); err == nil { + // get values + if _, err := loadControl.RequestLimitValues(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *UCLPP) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + loadControl, err := util.LoadControl(e.service, payload.Entity) + if err != nil { + return + } + + data, err := loadControl.GetLimitDescriptionsForCategory(model.LoadControlCategoryTypeObligation) + if err != nil { + return + } + + for _, item := range data { + if item.LimitId == nil { + continue + } + + _, err := loadControl.GetLimitValueForLimitId(*item.LimitId) + if err != nil { + continue + } + + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + return + } +} + +// the configuration key description data was updated +func (e *UCLPP) configurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := util.DeviceConfiguration(e.service, entity); err == nil { + // key value descriptions received, now get the data + if _, err := deviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the configuration key data was updated +func (e *UCLPP) configurationDataUpdate(payload spineapi.EventPayload) { + if _, err := e.FailsafeProductionActivePowerLimit(payload.Entity); err != nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeProductionActivePowerLimit) + } + if _, err := e.FailsafeDurationMinimum(payload.Entity); err != nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } +} diff --git a/uclpp/events_test.go b/uclpp/events_test.go new file mode 100644 index 0000000..b51014a --- /dev/null +++ b/uclpp/events_test.go @@ -0,0 +1,84 @@ +package uclpp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + 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 = eebusutil.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *UCLPPSuite) Test_Failures() { + s.sut.connected(s.mockRemoteEntity) + + s.sut.configurationDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *UCLPPSuite) Test_loadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.loadControlLimitDataUpdate(payload) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.loadControlLimitDataUpdate(payload) +} diff --git a/uclpp/public.go b/uclpp/public.go new file mode 100644 index 0000000..49e43c1 --- /dev/null +++ b/uclpp/public.go @@ -0,0 +1,304 @@ +package uclpp + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current loadcontrol limit data +// +// parameters: +// - entity: the entity of the e.g. EVSE +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCLPP) ProductionLimit(entity spineapi.EntityRemoteInterface) ( + limit api.LoadLimit, resultErr error) { + limit = api.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + } + + resultErr = api.ErrNoCompatibleEntity + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return + } + + resultErr = eebusapi.ErrDataNotAvailable + loadControl, err := util.LoadControl(e.service, entity) + if err != nil || loadControl == nil { + return + } + + limitDescriptions, err := loadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit) + if err != nil || len(limitDescriptions) != 1 { + return + } + + value, err := loadControl.GetLimitValueForLimitId(*limitDescriptions[0].LimitId) + if err != nil || value.Value == nil { + return + } + + limit.Value = value.Value.GetValue() + limit.IsChangeable = (value.IsLimitChangeable != nil && *value.IsLimitChangeable) + limit.IsActive = (value.IsLimitActive != nil && *value.IsLimitActive) + if value.TimePeriod != nil && value.TimePeriod.EndTime != nil { + if duration, err := value.TimePeriod.EndTime.GetTimeDuration(); err == nil { + limit.Duration = duration + } + } + + resultErr = nil + + return +} + +// send new LoadControlLimits +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - limit: load limit data +func (e *UCLPP) WriteProductionLimit( + entity spineapi.EntityRemoteInterface, + limit api.LoadLimit) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + loadControl, err := util.LoadControl(e.service, entity) + if err != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + limitDescriptions, err := loadControl.GetLimitDescriptionsForTypeCategoryDirectionScope( + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit, + ) + if err != nil || + len(limitDescriptions) != 1 || + limitDescriptions[0].LimitId == nil { + return nil, eebusapi.ErrMetadataNotAvailable + } + + limitDesc := limitDescriptions[0] + + if _, err := loadControl.GetLimitValueForLimitId(*limitDesc.LimitId); err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + currentLimits, err := loadControl.GetLimitValues() + if err != nil { + return nil, eebusapi.ErrDataNotAvailable + } + + for index, item := range currentLimits { + if item.LimitId == nil || + *item.LimitId != *limitDesc.LimitId { + continue + } + + // EEBus_UC_TS_LimitationOfPowerProduction V1.0.0 3.2.2.2.2.2 + // If set to "true", the timePeriod, value and isLimitActive Elements SHALL be writeable by a client. + if item.IsLimitChangeable != nil && !*item.IsLimitChangeable { + return nil, eebusapi.ErrNotSupported + } + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: eebusutil.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + newLimit.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + + currentLimits[index] = newLimit + break + } + + msgCounter, err := loadControl.WriteLimitValues(limitData) + + return msgCounter, err +} + +// Scenario 2 + +// return Failsafe limit for the produced active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPP) FailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + if err != nil || data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.ScaledNumberType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// send new Failsafe Production Active Power Limit +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - value: the new limit in W +func (e *UCLPP) WriteFailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil || data == nil || data.KeyId == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data.KeyId, + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + }, + }, + } + + msgCounter, err := deviceConfiguration.WriteKeyValues(keyData) + + return msgCounter, err +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *UCLPP) FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetKeyValueForKeyName(keyname, model.DeviceConfigurationKeyValueTypeTypeDuration) + if err != nil || data == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + value, ok := data.(*model.DurationType) + if !ok || value == nil { + return 0, eebusapi.ErrDataNotAvailable + } + + return value.GetTimeDuration() +} + +// send new Failsafe Duration Minimum +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - duration: the duration, between 2h and 24h +func (e *UCLPP) WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return nil, api.ErrNoCompatibleEntity + } + + if duration < time.Duration(time.Hour*2) || duration > time.Duration(time.Hour*24) { + return nil, errors.New("duration outside of allowed range") + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := util.DeviceConfiguration(e.service, entity) + if err != nil || deviceConfiguration == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + data, err := deviceConfiguration.GetDescriptionForKeyName(keyname) + if err != nil || data == nil || data.KeyId == nil { + return nil, eebusapi.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data.KeyId, + Value: &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(duration), + }, + }, + } + + msgCounter, err := deviceConfiguration.WriteKeyValues(keyData) + + return msgCounter, err +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// able to produce according to the device label or data sheet. +func (e *UCLPP) PowerProductionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) { + if entity == nil || !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return 0, api.ErrNoCompatibleEntity + } + + electricalConnection, err := util.ElectricalConnection(e.service, entity) + if err != nil || electricalConnection == nil { + return 0, err + } + + data, err := electricalConnection.GetCharacteristicForContextType( + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypePowerProductionNominalMax, + ) + if err != nil || data.Value == nil { + return 0, err + } + + return data.Value.GetValue(), nil +} diff --git a/uclpp/public_test.go b/uclpp/public_test.go new file mode 100644 index 0000000..9b31d19 --- /dev/null +++ b/uclpp/public_test.go @@ -0,0 +1,355 @@ +package uclpp + +import ( + "time" + + "github.com/enbility/cemd/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPSuite) Test_LoadControlLimit() { + data, err := s.sut.ProductionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + data, err = s.sut.ProductionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ProductionLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data.Value) + assert.Equal(s.T(), false, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(6000), + TimePeriod: &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeType("PT2H"), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ProductionLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 6000.0, data.Value) + assert.Equal(s.T(), true, data.IsChangeable) + assert.Equal(s.T(), false, data.IsActive) +} + +func (s *UCLPPSuite) Test_WriteLoadControlLimit() { + limit := api.LoadLimit{ + Value: 6000, + IsActive: true, + Duration: 0, + } + _, err := s.sut.WriteProductionLimit(s.mockRemoteEntity, limit) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: eebusutil.Ptr(true), + IsLimitActive: eebusutil.Ptr(false), + Value: model.NewScaledNumberType(6000), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limit.Duration = time.Duration(time.Hour * 2) + _, err = s.sut.WriteProductionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) +} + +func (s *UCLPPSuite) Test_FailsafeProductionActivePowerLimit() { + data, err := s.sut.FailsafeProductionActivePowerLimit(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(4000), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeProductionActivePowerLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 4000.0, data) +} + +func (s *UCLPPSuite) Test_WriteFailsafeProductionActivePowerLimit() { + _, err := s.sut.WriteFailsafeProductionActivePowerLimit(s.mockRemoteEntity, 6000) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeProductionActivePowerLimit(s.monitoredEntity, 6000) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeProductionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeProductionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) +} + +func (s *UCLPPSuite) Test_FailsafeDurationMinimum() { + data, err := s.sut.FailsafeDurationMinimum(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), time.Duration(0), data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(time.Hour * 2), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeDurationMinimum(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), time.Duration(time.Hour*2), data) +} + +func (s *UCLPPSuite) Test_WriteFailsafeDurationMinimum() { + _, err := s.sut.WriteFailsafeDurationMinimum(s.mockRemoteEntity, time.Duration(time.Hour*2)) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*2)) + assert.Nil(s.T(), err) + + _, err = s.sut.WriteFailsafeDurationMinimum(s.monitoredEntity, time.Duration(time.Hour*1)) + assert.NotNil(s.T(), err) +} + +func (s *UCLPPSuite) Test_PowerProductionNominalMax() { + data, err := s.sut.PowerProductionNominalMax(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerProductionNominalMax(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + charData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + CharacteristicId: eebusutil.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypePowerProductionNominalMax), + Value: model.NewScaledNumberType(8000), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionCharacteristicListData, charData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerProductionNominalMax(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 8000.0, data) +} diff --git a/uclpp/testhelper_test.go b/uclpp/testhelper_test.go new file mode 100644 index 0000000..dd0a80c --- /dev/null +++ b/uclpp/testhelper_test.go @@ -0,0 +1,182 @@ +package uclpp + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPPSuite(t *testing.T) { + suite.Run(t, new(UCLPPSuite)) +} + +type UCLPPSuite struct { + suite.Suite + + sut *UCLPP + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *UCLPPSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCLPPSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.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() + + s.sut = NewUCLPP(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.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.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionCharacteristicListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeEVSE), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/uclpp/types.go b/uclpp/types.go new file mode 100644 index 0000000..f54c515 --- /dev/null +++ b/uclpp/types.go @@ -0,0 +1,38 @@ +package uclpp + +import "github.com/enbility/cemd/api" + +const ( + // Load control obligation limit data updated + // + // The callback with this message provides: + // - the device of the e.g. EVSE + // - the entity of the e.g. EVSE + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" + + // Failsafe limit for the produced active (real) power of the + // Controllable System data updated + // + // The callback with this message provides: + // - the device of the e.g. EVSE + // - the entity of the e.g. EVSE + // + // Use Case LPC, Scenario 2 + // + // Note: the referred data may be updated together with all other configuration items of this use case + DataUpdateFailsafeProductionActivePowerLimit api.EventType = "DataUpdateFailsafeProductionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data updated + // + // The callback with this message provides: + // - the device of the e.g. EVSE + // - the entity of the e.g. EVSE + // + // Use Case LPC, Scenario 2 + // + // Note: the referred data may be updated together with all other configuration items of this use case + DataUpdateFailsafeDurationMinimum api.EventType = "DataUpdateFailsafeDurationMinimum" +) diff --git a/uclpp/uclpp.go b/uclpp/uclpp.go new file mode 100644 index 0000000..798faf5 --- /dev/null +++ b/uclpp/uclpp.go @@ -0,0 +1,119 @@ +package uclpp + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCLPP struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ UCLPPInterface = (*UCLPP)(nil) + +func NewUCLPP(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPP { + uc := &UCLPP{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeEVSE, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCLPP) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeLimitationOfPowerProduction +} + +func (e *UCLPP) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = localEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +func (e *UCLPP) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (e *UCLPP) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeEnergyGuard, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCLPP) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + }, + ) { + return false, nil + } + + if _, err := util.DeviceDiagnosis(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := util.LoadControl(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + if _, err := util.DeviceConfiguration(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/uclpp/uclpp_test.go b/uclpp/uclpp_test.go new file mode 100644 index 0000000..0c90cec --- /dev/null +++ b/uclpp/uclpp_test.go @@ -0,0 +1,45 @@ +package uclpp + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCLPPSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeLimitationOfPowerProduction), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/uclppserver/api.go b/uclppserver/api.go new file mode 100644 index 0000000..8df3440 --- /dev/null +++ b/uclppserver/api.go @@ -0,0 +1,80 @@ +package uclppserver + +import ( + "time" + + "github.com/enbility/cemd/api" +) + +//go:generate mockery + +// interface for the Limitation of Power Production UseCase +type UCLPPServerInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current loadcontrol limit data + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ProductionLimit() (api.LoadLimit, error) + + // set the current loadcontrol limit data + SetProductionLimit(limit api.LoadLimit) (resultErr error) + + // Scenario 2 + + // return Failsafe limit for the produced active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // return values: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + FailsafeProductionActivePowerLimit() (value float64, isChangeable bool, resultErr error) + + // set Failsafe limit for the produced active (real) power of the + // Controllable System. This limit becomes activated in "init" state or "failsafe state". + // + // parameters: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + SetFailsafeProductionActivePowerLimit(value float64, changeable bool) (resultErr error) + + // return minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // return values: + // - value: the power limit in W + // - changeable: boolean if the client service can change the limit + FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) + + // set minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" + // + // parameters: + // - duration: has to be >= 2h and <= 24h + // - changeable: boolean if the client service can change this value + SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) + + // Scenario 3 + + // this is automatically covered by the SPINE implementation + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // allowed to produce due to the customer's contract. + ContractualProductionNominalMax() (float64, error) + + // set nominal maximum active (real) power the Controllable System is + // allowed to produce due to the customer's contract. + // + // parameters: + // - value: contractual nominal max power production in W + SetContractualProductionNominalMax(value float64) (resultErr error) +} diff --git a/uclppserver/events.go b/uclppserver/events.go new file mode 100644 index 0000000..49ecba5 --- /dev/null +++ b/uclppserver/events.go @@ -0,0 +1,152 @@ +package uclppserver + +import ( + "slices" + + "github.com/enbility/cemd/util" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *UCLPPServer) HandleEvent(payload spineapi.EventPayload) { + if util.IsDeviceConnected(payload) { + e.deviceConnected(payload) + return + } + + if !util.IsCompatibleEntity(payload.Entity, e.validEntityTypes) { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // did we receive a binding to the loadControl server and the + // heartbeatWorkaround is required? + if payload.EventType == spineapi.EventTypeBindingChange && + payload.ChangeType == spineapi.ElementChangeAdd && + payload.LocalFeature != nil && + payload.LocalFeature.Type() == model.FeatureTypeTypeLoadControl && + payload.LocalFeature.Role() == model.RoleTypeServer { + e.subscribeHeartbeatWorkaround(payload) + return + } + + if localEntity == nil || + payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate || + payload.CmdClassifier == nil || + *payload.CmdClassifier != model.CmdClassifierTypeWrite { + return + } + + // the codefactor warning is invalid, as .(type) check can not be replaced with if then + //revive:disable-next-line + switch payload.Data.(type) { + case *model.LoadControlLimitListDataType: + serverF := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeLoadControlLimitListData || + payload.LocalFeature != serverF { + return + } + + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueListDataType: + serverF := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeDeviceConfigurationKeyValueListData || + payload.LocalFeature != serverF { + return + } + + e.configurationDataUpdate(payload) + } +} + +// a remote device was connected and we know its entities +func (e *UCLPPServer) deviceConnected(payload spineapi.EventPayload) { + if payload.Device == nil { + return + } + + // check if there is a DeviceDiagnosis server on one or more entities + remoteDevice := payload.Device + + var deviceDiagEntites []spineapi.EntityRemoteInterface + + entites := remoteDevice.Entities() + for _, entity := range entites { + if !slices.Contains(e.validEntityTypes, entity.EntityType()) { + continue + } + + deviceDiagF := entity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + if deviceDiagF == nil { + continue + } + + deviceDiagEntites = append(deviceDiagEntites, entity) + } + + // the remote device does not have a DeviceDiagnosis Server, which it should + if len(deviceDiagEntites) == 0 { + return + } + + // we only found one matching entity, as it should be, subscribe + if len(deviceDiagEntites) == 1 { + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, deviceDiagEntites[0]); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := localDeviceDiag.RequestHeartbeat(); err != nil { + logging.Log().Debug(err) + } + } + + return + } + + // we found more than one matching entity, this is not good + // according to KEO the subscription should be done on the entity that requests a binding to + // the local loadControlLimit server feature + e.heartbeatKeoWorkaround = true +} + +// subscribe to the DeviceDiagnosis Server of the entity that created a binding +func (e *UCLPPServer) subscribeHeartbeatWorkaround(payload spineapi.EventPayload) { + // the workaround is not needed, exit + if !e.heartbeatKeoWorkaround { + return + } + + if localDeviceDiag, err := util.DeviceDiagnosis(e.service, payload.Entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := localDeviceDiag.RequestHeartbeat(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *UCLPPServer) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if _, err := e.ProductionLimit(); err != nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the configuration key data of an SMGW was updated +func (e *UCLPPServer) configurationDataUpdate(payload spineapi.EventPayload) { + if _, _, err := e.FailsafeProductionActivePowerLimit(); err != nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeProductionActivePowerLimit) + } + if _, _, err := e.FailsafeDurationMinimum(); err != nil { + e.eventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } +} diff --git a/uclppserver/events_test.go b/uclppserver/events_test.go new file mode 100644 index 0000000..3307de5 --- /dev/null +++ b/uclppserver/events_test.go @@ -0,0 +1,204 @@ +package uclppserver + +import ( + "fmt" + + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" +) + +func (s *UCLPPServerSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Device = s.monitoredEntity.Device() + payload.Entity = s.monitoredEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.CmdClassifier = eebusutil.Ptr(model.CmdClassifierTypeWrite) + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeLoadControlLimitListData + payload.Data = eebusutil.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) + + payload.Function = model.FunctionTypeDeviceConfigurationKeyValueListData + payload.Data = eebusutil.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.deviceConfigurationFeature + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeBindingChange + payload.ChangeType = spineapi.ElementChangeAdd + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) +} + +func (s *UCLPPServerSuite) Test_deviceConnected() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + + s.sut.deviceConnected(payload) + + // no entities + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().Entities().Return(nil) + payload.Device = mockRemoteDevice + s.sut.deviceConnected(payload) + + // one entity with one DeviceDiagnosis server + payload.Device = s.remoteDevice + s.sut.deviceConnected(payload) + + s.sut.subscribeHeartbeatWorkaround(payload) +} + +func (s *UCLPPServerSuite) Test_multipleDeviceDiagServer() { + // multiple entities each with DeviceDiagnosis server + + payload := spineapi.EventPayload{ + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + // 4 entites + for i := 1; i < 5; i++ { + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{model.AddressEntityType(i)}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{2}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{3}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{4}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeCEM), + }, + }, + }, + FeatureInformation: featureInformations, + } + + _, err := s.remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + s.remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + s.sut.deviceConnected(payload) + + s.sut.subscribeHeartbeatWorkaround(payload) +} diff --git a/uclppserver/public.go b/uclppserver/public.go new file mode 100644 index 0000000..5cbab72 --- /dev/null +++ b/uclppserver/public.go @@ -0,0 +1,206 @@ +package uclppserver + +import ( + "errors" + "time" + + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" +) + +// Scenario 1 + +// return the current production limit data +// +// return values: +// - limit: load limit data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *UCLPPServer) ProductionLimit() (limit api.LoadLimit, resultErr error) { + limit = api.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + Duration: 0, + } + resultErr = eebusapi.ErrDataNotAvailable + + description := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope( + e.service, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit, + ) + if description.LimitId == nil { + return + } + + value := util.GetLocalLimitValueForLimitId(e.service, *description.LimitId) + if value.LimitId == nil || value.Value == nil { + return + } + + limit.Value = value.Value.GetValue() + limit.IsChangeable = (value.IsLimitChangeable != nil && *value.IsLimitChangeable) + limit.IsActive = (value.IsLimitActive != nil && *value.IsLimitActive) + if value.TimePeriod != nil && value.TimePeriod.EndTime != nil { + if duration, err := value.TimePeriod.EndTime.GetTimeDuration(); err == nil { + limit.Duration = duration + } + } + + return limit, nil +} + +// set the current production limit data +func (e *UCLPPServer) SetProductionLimit(limit api.LoadLimit) (resultErr error) { + resultErr = eebusapi.ErrDataNotAvailable + + description := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope( + e.service, + model.LoadControlLimitTypeTypeSignDependentAbsValueLimit, + model.LoadControlCategoryTypeObligation, + model.EnergyDirectionTypeProduce, + model.ScopeTypeTypeActivePowerLimit, + ) + if description.LimitId == nil { + return + } + + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + loadControl := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + if loadControl == nil { + return + } + + limitData := model.LoadControlLimitDataType{ + LimitId: description.LimitId, + IsLimitChangeable: eebusutil.Ptr(limit.IsChangeable), + IsLimitActive: eebusutil.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + limitData.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + limits := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{limitData}, + } + + loadControl.SetData(model.FunctionTypeLoadControlLimitListData, limits) + + return nil +} + +// Scenario 2 + +// return Failsafe limit for the produced active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPPServer) FailsafeProductionActivePowerLimit() (limit float64, isChangeable bool, resultErr error) { + limit = 0 + isChangeable = false + resultErr = eebusapi.ErrDataNotAvailable + + keyName := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + keyData := util.GetLocalDeviceConfigurationKeyValueForKeyName(e.service, keyName) + if keyData.KeyId == nil || keyData.Value == nil || keyData.Value.ScaledNumber == nil { + return + } + + limit = keyData.Value.ScaledNumber.GetValue() + isChangeable = (keyData.IsValueChangeable != nil && *keyData.IsValueChangeable) + resultErr = nil + return +} + +// set Failsafe limit for the produced active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *UCLPPServer) SetFailsafeProductionActivePowerLimit(value float64, changeable bool) error { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + keyValue := model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + } + + return util.SetLocalDeviceConfigurationKeyValue(e.service, keyName, changeable, keyValue) +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *UCLPPServer) FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) { + duration = 0 + isChangeable = false + resultErr = eebusapi.ErrDataNotAvailable + + keyName := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + keyData := util.GetLocalDeviceConfigurationKeyValueForKeyName(e.service, keyName) + if keyData.KeyId == nil || keyData.Value == nil || keyData.Value.Duration == nil { + return + } + + durationValue, err := keyData.Value.Duration.GetTimeDuration() + if err != nil { + return + } + + duration = durationValue + isChangeable = (keyData.IsValueChangeable != nil && *keyData.IsValueChangeable) + resultErr = nil + return +} + +// set minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +// +// parameters: +// - duration: has to be >= 2h and <= 24h +// - changeable: boolean if the client service can change this value +func (e *UCLPPServer) SetFailsafeDurationMinimum(duration time.Duration, changeable bool) error { + if duration < time.Duration(time.Hour*2) || duration > time.Duration(time.Hour*24) { + return errors.New("duration outside of allowed range") + } + keyName := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + keyValue := model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(duration), + } + + return util.SetLocalDeviceConfigurationKeyValue(e.service, keyName, changeable, keyValue) +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// allowed to produce due to the customer's contract. +func (e *UCLPPServer) ContractualProductionNominalMax() (value float64, resultErr error) { + value = 0 + resultErr = eebusapi.ErrDataNotAvailable + + charData := util.GetLocalElectricalConnectionCharacteristicForContextType( + e.service, + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax, + ) + if charData.CharacteristicId == nil || charData.Value == nil { + return + } + + return charData.Value.GetValue(), nil +} + +// set nominal maximum active (real) power the Controllable System is +// allowed to produce due to the customer's contract. +func (e *UCLPPServer) SetContractualProductionNominalMax(value float64) error { + return util.SetLocalElectricalConnectionCharacteristicForContextType( + e.service, + model.ElectricalConnectionCharacteristicContextTypeEntity, + model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax, + value, + ) +} diff --git a/uclppserver/public_test.go b/uclppserver/public_test.go new file mode 100644 index 0000000..fa903a1 --- /dev/null +++ b/uclppserver/public_test.go @@ -0,0 +1,77 @@ +package uclppserver + +import ( + "time" + + "github.com/enbility/cemd/api" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPServerSuite) Test_LoadControlLimit() { + limit, err := s.sut.ProductionLimit() + assert.Equal(s.T(), 0.0, limit.Value) + assert.NotNil(s.T(), err) + + newLimit := api.LoadLimit{ + Duration: time.Duration(time.Hour * 2), + IsActive: true, + IsChangeable: true, + Value: 16, + } + err = s.sut.SetProductionLimit(newLimit) + assert.Nil(s.T(), err) + + limit, err = s.sut.ProductionLimit() + assert.Equal(s.T(), 16.0, limit.Value) + assert.Nil(s.T(), err) +} + +func (s *UCLPPServerSuite) Test_Failsafe() { + limit, changeable, err := s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 0.0, limit) + assert.Equal(s.T(), false, changeable) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeProductionActivePowerLimit(10, true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 10.0, limit) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) + + // The actual tests of the functionality is located in the util package + duration, changeable, err := s.sut.FailsafeDurationMinimum() + assert.Equal(s.T(), time.Duration(0), duration) + assert.Equal(s.T(), false, changeable) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeDurationMinimum(time.Duration(time.Hour*1), true) + assert.NotNil(s.T(), err) + + err = s.sut.SetFailsafeDurationMinimum(time.Duration(time.Hour*2), true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 10.0, limit) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) + + duration, changeable, err = s.sut.FailsafeDurationMinimum() + assert.Equal(s.T(), time.Duration(time.Hour*2), duration) + assert.Equal(s.T(), true, changeable) + assert.Nil(s.T(), err) +} + +func (s *UCLPPServerSuite) Test_ContractualProductionNominalMax() { + value, err := s.sut.ContractualProductionNominalMax() + assert.Equal(s.T(), 0.0, value) + assert.NotNil(s.T(), err) + + err = s.sut.SetContractualProductionNominalMax(10) + assert.Nil(s.T(), err) + + value, err = s.sut.ContractualProductionNominalMax() + assert.Equal(s.T(), 10.0, value) + assert.Nil(s.T(), err) +} diff --git a/uclppserver/testhelper_test.go b/uclppserver/testhelper_test.go new file mode 100644 index 0000000..fd9e33d --- /dev/null +++ b/uclppserver/testhelper_test.go @@ -0,0 +1,197 @@ +package uclppserver + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/cemd/api" + eebusapi "github.com/enbility/eebus-go/api" + eebusmocks "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPPServerSuite(t *testing.T) { + suite.Run(t, new(UCLPPServerSuite)) +} + +type UCLPPServerSuite struct { + suite.Suite + + sut *UCLPPServer + + service eebusapi.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *mocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface +} + +func (s *UCLPPServerSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UCLPPServerSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := eebusapi.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := eebusmocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := mocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = mocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := mocks.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() + + s.sut = NewUCLPP(s.service, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + localEntity := s.sut.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.loadControlFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + s.deviceDiagnosisFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + s.deviceConfigurationFeature = localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService eebusapi.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + f := spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionParameterDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionPermittedValueSetListData, true, false) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, true) + localEntity.AddFeature(f) + + 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.FeatureTypeTypeLoadControl, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeClient, + []model.FunctionType{}, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisHeartbeatData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeClient, + []model.FunctionType{}, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: eebusutil.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: eebusutil.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: eebusutil.Ptr(feature.featureType), + Role: eebusutil.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: eebusutil.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: eebusutil.Ptr(model.EntityTypeTypeGridGuard), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/uclppserver/types.go b/uclppserver/types.go new file mode 100644 index 0000000..e780589 --- /dev/null +++ b/uclppserver/types.go @@ -0,0 +1,38 @@ +package uclppserver + +import "github.com/enbility/cemd/api" + +const ( + // Load control obligation limit data updated + // + // The callback with this message provides: + // - the device of the e.g. SMGW + // - the entity of the e.g. SMGW + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "DataUpdateLimit" + + // Failsafe limit for the produced active (real) power of the + // Controllable System data updated + // + // The callback with this message provides: + // - the device of the e.g. SMGW + // - the entity of the e.g. SMGW + // + // Use Case LPC, Scenario 2 + // + // Note: the referred data may be updated together with all other configuration items of this use case + DataUpdateFailsafeProductionActivePowerLimit api.EventType = "DataUpdateFailsafeProductionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data updated + // + // The callback with this message provides: + // - the device of the e.g. SMGW + // - the entity of the e.g. SMGW + // + // Use Case LPC, Scenario 2 + // + // Note: the referred data may be updated together with all other configuration items of this use case + DataUpdateFailsafeDurationMinimum api.EventType = "DataUpdateFailsafeDurationMinimum" +) diff --git a/uclppserver/uclpp.go b/uclppserver/uclpp.go new file mode 100644 index 0000000..3c0ad5b --- /dev/null +++ b/uclppserver/uclpp.go @@ -0,0 +1,208 @@ +package uclppserver + +import ( + "github.com/enbility/cemd/api" + "github.com/enbility/cemd/util" + eebusapi "github.com/enbility/eebus-go/api" + eebusutil "github.com/enbility/eebus-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type UCLPPServer struct { + service eebusapi.ServiceInterface + + eventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType + + heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use +} + +var _ UCLPPServerInterface = (*UCLPPServer)(nil) + +func NewUCLPP(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPPServer { + uc := &UCLPPServer{ + service: service, + eventCB: eventCB, + } + + uc.validEntityTypes = []model.EntityTypeType{ + model.EntityTypeTypeGridGuard, + model.EntityTypeTypeCEM, // KEO uses this entity type for an SMGW whysoever + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (c *UCLPPServer) UseCaseName() model.UseCaseNameType { + return model.UseCaseNameTypeLimitationOfPowerProduction +} + +func (e *UCLPPServer) AddFeatures() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + // client features + _ = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + // server features + f := localEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + + var limitId model.LoadControlLimitIdType = 0 + // get the highest limitId + if desc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( + f, model.FunctionTypeLoadControlLimitDescriptionListData); err == nil && desc.LoadControlLimitDescriptionData != nil { + for _, desc := range desc.LoadControlLimitDescriptionData { + if desc.LimitId != nil && *desc.LimitId >= limitId { + limitId++ + } + } + } + + loadControlDesc := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + f.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, loadControlDesc) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + + var configId model.DeviceConfigurationKeyIdType = 0 + // get the highest keyId + if desc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( + f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData); err == nil && desc.DeviceConfigurationKeyValueDescriptionData != nil { + for _, desc := range desc.DeviceConfigurationKeyValueDescriptionData { + if desc.KeyId != nil && *desc.KeyId >= configId { + configId++ + } + } + } + + deviceConfigDesc := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + }, + } + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, deviceConfigDesc) + + deviceConfig := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + IsValueChangeable: eebusutil.Ptr(true), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + IsValueChangeable: eebusutil.Ptr(true), + }, + }, + } + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, deviceConfig) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) + + f = localEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, false) + + var elCharId model.ElectricalConnectionCharacteristicIdType = 0 + // get the highest CharacteristicId + if desc, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( + f, model.FunctionTypeElectricalConnectionCharacteristicListData); err == nil && desc.ElectricalConnectionCharacteristicData != nil { + for _, desc := range desc.ElectricalConnectionCharacteristicData { + if desc.CharacteristicId != nil && *desc.CharacteristicId >= elCharId { + elCharId++ + } + } + } + + // ElectricalConnectionId and ParameterId should be identical to the ones used + // in a MPC Server role implementation, which is not done here (yet) + elCharData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(elCharId), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + }, + } + f.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, elCharData) +} + +func (e *UCLPPServer) AddUseCase() { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.AddUseCaseSupport( + model.UseCaseActorTypeControllableSystem, + e.UseCaseName(), + model.SpecificationVersionType("1.0.0"), + "release", + true, + []model.UseCaseScenarioSupportType{1, 2, 3, 4}) +} + +func (e *UCLPPServer) UpdateUseCaseAvailability(available bool) { + localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + + localEntity.SetUseCaseAvailability(model.UseCaseActorTypeControllableSystem, e.UseCaseName(), available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *UCLPPServer) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !util.IsCompatibleEntity(entity, e.validEntityTypes) { + return false, api.ErrNoCompatibleEntity + } + + // check if the usecase and mandatory scenarios are supported and + // if the required server features are available + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEnergyGuard, + e.UseCaseName(), + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + }, + ) { + return false, nil + } + + if _, err := util.DeviceDiagnosis(e.service, entity); err != nil { + return false, eebusapi.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/uclppserver/uclpp_test.go b/uclppserver/uclpp_test.go new file mode 100644 index 0000000..1d5ba50 --- /dev/null +++ b/uclppserver/uclpp_test.go @@ -0,0 +1,45 @@ +package uclppserver + +import ( + eebusutil "github.com/enbility/eebus-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UCLPPServerSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *UCLPPServerSuite) Test_IsUseCaseSupported() { + data, err := s.sut.IsUseCaseSupported(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: eebusutil.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: eebusutil.Ptr(model.UseCaseNameTypeLimitationOfPowerProduction), + UseCaseAvailable: eebusutil.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + }, + }, + }, + }, + } + + nodemgmtEntity := s.remoteDevice.Entity([]model.AddressEntityType{0}) + nodeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(nodemgmtEntity, model.FeatureTypeTypeNodeManagement, model.RoleTypeSpecial) + fErr := nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} From 072fc37a370056e5ac264ce1a21c994a188768f0 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Tue, 16 Apr 2024 17:20:46 +0200 Subject: [PATCH 2/2] Support UCLPC and UCLPP server running together --- cmd/democem/democem.go | 21 +++++++ uclpcserver/uclpc.go | 133 ++++++++++++++++++++++++----------------- uclppserver/uclpp.go | 133 ++++++++++++++++++++++++----------------- 3 files changed, 175 insertions(+), 112 deletions(-) diff --git a/cmd/democem/democem.go b/cmd/democem/democem.go index 1817aa8..d4e2306 100644 --- a/cmd/democem/democem.go +++ b/cmd/democem/democem.go @@ -8,6 +8,7 @@ import ( "github.com/enbility/cemd/cem" "github.com/enbility/cemd/ucevsecc" "github.com/enbility/cemd/uclpcserver" + "github.com/enbility/cemd/uclppserver" eebusapi "github.com/enbility/eebus-go/api" "github.com/enbility/ship-go/logging" ) @@ -53,6 +54,26 @@ func (d *DemoCem) Setup() error { logging.Log().Error(err) } + lpps := uclppserver.NewUCLPP(d.cem.Service, d.entityEventCB) + d.cem.AddUseCase(lpps) + + if err := lpps.SetProductionLimit(api.LoadLimit{ + IsChangeable: true, + IsActive: false, + Value: 0, + }); err != nil { + logging.Log().Error(err) + } + if err := lpps.SetContractualProductionNominalMax(-7000); err != nil { + logging.Log().Error(err) + } + if err := lpps.SetFailsafeProductionActivePowerLimit(0, true); err != nil { + logging.Log().Error(err) + } + if err := lpps.SetFailsafeDurationMinimum(time.Hour*2, true); err != nil { + logging.Log().Error(err) + } + evsecc := ucevsecc.NewUCEVSECC(d.cem.Service, d.entityEventCB) d.cem.AddUseCase(evsecc) diff --git a/uclpcserver/uclpc.go b/uclpcserver/uclpc.go index 8d86939..4dff596 100644 --- a/uclpcserver/uclpc.go +++ b/uclpcserver/uclpc.go @@ -55,8 +55,9 @@ func (e *UCLPCServer) AddFeatures() { var limitId model.LoadControlLimitIdType = 0 // get the highest limitId - if desc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( - f, model.FunctionTypeLoadControlLimitDescriptionListData); err == nil && desc.LoadControlLimitDescriptionData != nil { + desc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( + f, model.FunctionTypeLoadControlLimitDescriptionListData) + if err == nil && desc.LoadControlLimitDescriptionData != nil { for _, desc := range desc.LoadControlLimitDescriptionData { if desc.LimitId != nil && *desc.LimitId >= limitId { limitId++ @@ -64,20 +65,23 @@ func (e *UCLPCServer) AddFeatures() { } } - loadControlDesc := &model.LoadControlLimitDescriptionListDataType{ - LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ - { - LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), - LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), - LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), - LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), - MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( - Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), - ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), - }, - }, + if desc == nil || len(desc.LoadControlLimitDescriptionData) == 0 { + desc = &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{}, + } } - f.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, loadControlDesc) + + newLimitDesc := model.LoadControlLimitDescriptionDataType{ + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeConsume), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + desc.LoadControlLimitDescriptionData = append(desc.LoadControlLimitDescriptionData, newLimitDesc) + f.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, desc) f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) @@ -85,45 +89,58 @@ func (e *UCLPCServer) AddFeatures() { var configId model.DeviceConfigurationKeyIdType = 0 // get the highest keyId - if desc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( - f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData); err == nil && desc.DeviceConfigurationKeyValueDescriptionData != nil { - for _, desc := range desc.DeviceConfigurationKeyValueDescriptionData { + deviceConfigDesc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( + f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData) + if err == nil && deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData != nil { + for _, desc := range deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData { if desc.KeyId != nil && *desc.KeyId >= configId { configId++ } } } - deviceConfigDesc := &model.DeviceConfigurationKeyValueDescriptionListDataType{ - DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), - KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), - ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), - Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), - }, - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), - KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), - ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), - }, + if err != nil || deviceConfigDesc == nil || len(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData) == 0 { + deviceConfigDesc = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{}, + } + } + + newConfigs := []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), }, } + deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData = append(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData, newConfigs...) f.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, deviceConfigDesc) - deviceConfig := &model.DeviceConfigurationKeyValueListDataType{ - DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), - IsValueChangeable: eebusutil.Ptr(true), - }, - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), - IsValueChangeable: eebusutil.Ptr(true), - }, + configData, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueListDataType](f, model.FunctionTypeDeviceConfigurationKeyValueListData) + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + configData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + } + + newConfigData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + IsValueChangeable: eebusutil.Ptr(true), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + IsValueChangeable: eebusutil.Ptr(true), }, } - f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, deviceConfig) + + configData.DeviceConfigurationKeyValueData = append(configData.DeviceConfigurationKeyValueData, newConfigData...) + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, configData) f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) @@ -133,29 +150,33 @@ func (e *UCLPCServer) AddFeatures() { var elCharId model.ElectricalConnectionCharacteristicIdType = 0 // get the highest CharacteristicId - if desc, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( - f, model.FunctionTypeElectricalConnectionCharacteristicListData); err == nil && desc.ElectricalConnectionCharacteristicData != nil { - for _, desc := range desc.ElectricalConnectionCharacteristicData { + elCharData, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( + f, model.FunctionTypeElectricalConnectionCharacteristicListData) + if err == nil && elCharData.ElectricalConnectionCharacteristicData != nil { + for _, desc := range elCharData.ElectricalConnectionCharacteristicData { if desc.CharacteristicId != nil && *desc.CharacteristicId >= elCharId { elCharId++ } } } + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + elCharData = &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{}, + } + } + // ElectricalConnectionId and ParameterId should be identical to the ones used // in a MPC Server role implementation, which is not done here (yet) - elCharData := &model.ElectricalConnectionCharacteristicListDataType{ - ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ - { - ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), - ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), - CharacteristicId: eebusutil.Ptr(elCharId), - CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), - CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax), - Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), - }, - }, + newCharData := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(elCharId), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), } + elCharData.ElectricalConnectionCharacteristicData = append(elCharData.ElectricalConnectionCharacteristicData, newCharData) f.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, elCharData) } diff --git a/uclppserver/uclpp.go b/uclppserver/uclpp.go index 3c0ad5b..e5a615f 100644 --- a/uclppserver/uclpp.go +++ b/uclppserver/uclpp.go @@ -55,28 +55,32 @@ func (e *UCLPPServer) AddFeatures() { var limitId model.LoadControlLimitIdType = 0 // get the highest limitId - if desc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( - f, model.FunctionTypeLoadControlLimitDescriptionListData); err == nil && desc.LoadControlLimitDescriptionData != nil { - for _, desc := range desc.LoadControlLimitDescriptionData { + loadControlDesc, err := spine.LocalFeatureDataCopyOfType[*model.LoadControlLimitDescriptionListDataType]( + f, model.FunctionTypeLoadControlLimitDescriptionListData) + if err == nil && loadControlDesc.LoadControlLimitDescriptionData != nil { + for _, desc := range loadControlDesc.LoadControlLimitDescriptionData { if desc.LimitId != nil && *desc.LimitId >= limitId { limitId++ } } } - loadControlDesc := &model.LoadControlLimitDescriptionListDataType{ - LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ - { - LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), - LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), - LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), - LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), - MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( - Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), - ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), - }, - }, + if loadControlDesc == nil || len(loadControlDesc.LoadControlLimitDescriptionData) == 0 { + loadControlDesc = &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{}, + } } + + newLimitDesc := model.LoadControlLimitDescriptionDataType{ + LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(limitId)), + LimitType: eebusutil.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: eebusutil.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: eebusutil.Ptr(model.EnergyDirectionTypeProduce), + MeasurementId: eebusutil.Ptr(model.MeasurementIdType(0)), // This is a fake Measurement ID, as there is no Electrical Connection server defined, it can't provide any meaningful. But KEO requires this to be set :( + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: eebusutil.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + loadControlDesc.LoadControlLimitDescriptionData = append(loadControlDesc.LoadControlLimitDescriptionData, newLimitDesc) f.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, loadControlDesc) f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) @@ -85,45 +89,58 @@ func (e *UCLPPServer) AddFeatures() { var configId model.DeviceConfigurationKeyIdType = 0 // get the highest keyId - if desc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( - f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData); err == nil && desc.DeviceConfigurationKeyValueDescriptionData != nil { - for _, desc := range desc.DeviceConfigurationKeyValueDescriptionData { + deviceConfigDesc, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueDescriptionListDataType]( + f, model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData) + if err == nil && deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData != nil { + for _, desc := range deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData { if desc.KeyId != nil && *desc.KeyId >= configId { configId++ } } } - deviceConfigDesc := &model.DeviceConfigurationKeyValueDescriptionListDataType{ - DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), - KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), - ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), - Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), - }, - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), - KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), - ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), - }, + if deviceConfigDesc == nil || len(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData) == 0 { + deviceConfigDesc = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{}, + } + } + + newConfigs := []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + KeyName: eebusutil.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: eebusutil.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), }, } + deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData = append(deviceConfigDesc.DeviceConfigurationKeyValueDescriptionData, newConfigs...) f.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, deviceConfigDesc) - deviceConfig := &model.DeviceConfigurationKeyValueListDataType{ - DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), - IsValueChangeable: eebusutil.Ptr(true), - }, - { - KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), - IsValueChangeable: eebusutil.Ptr(true), - }, + configData, err := spine.LocalFeatureDataCopyOfType[*model.DeviceConfigurationKeyValueListDataType](f, model.FunctionTypeDeviceConfigurationKeyValueListData) + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + configData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + } + + newConfigData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId)), + IsValueChangeable: eebusutil.Ptr(true), + }, + { + KeyId: eebusutil.Ptr(model.DeviceConfigurationKeyIdType(configId + 1)), + IsValueChangeable: eebusutil.Ptr(true), }, } - f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, deviceConfig) + + configData.DeviceConfigurationKeyValueData = append(configData.DeviceConfigurationKeyValueData, newConfigData...) + f.SetData(model.FunctionTypeDeviceConfigurationKeyValueListData, configData) f = localEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) @@ -133,29 +150,33 @@ func (e *UCLPPServer) AddFeatures() { var elCharId model.ElectricalConnectionCharacteristicIdType = 0 // get the highest CharacteristicId - if desc, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( - f, model.FunctionTypeElectricalConnectionCharacteristicListData); err == nil && desc.ElectricalConnectionCharacteristicData != nil { - for _, desc := range desc.ElectricalConnectionCharacteristicData { + elCharData, err := spine.LocalFeatureDataCopyOfType[*model.ElectricalConnectionCharacteristicListDataType]( + f, model.FunctionTypeElectricalConnectionCharacteristicListData) + if err == nil && elCharData.ElectricalConnectionCharacteristicData != nil { + for _, desc := range elCharData.ElectricalConnectionCharacteristicData { if desc.CharacteristicId != nil && *desc.CharacteristicId >= elCharId { elCharId++ } } } + if err != nil || configData == nil || len(configData.DeviceConfigurationKeyValueData) == 0 { + elCharData = &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{}, + } + } + // ElectricalConnectionId and ParameterId should be identical to the ones used // in a MPC Server role implementation, which is not done here (yet) - elCharData := &model.ElectricalConnectionCharacteristicListDataType{ - ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ - { - ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), - ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), - CharacteristicId: eebusutil.Ptr(elCharId), - CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), - CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), - Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), - }, - }, + newCharData := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: eebusutil.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: eebusutil.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicId: eebusutil.Ptr(elCharId), + CharacteristicContext: eebusutil.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: eebusutil.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), + Unit: eebusutil.Ptr(model.UnitOfMeasurementTypeW), } + elCharData.ElectricalConnectionCharacteristicData = append(elCharData.ElectricalConnectionCharacteristicData, newCharData) f.SetData(model.FunctionTypeElectricalConnectionCharacteristicListData, elCharData) }