diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 6a41f08d..ecc2341b 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -32,15 +32,15 @@ jobs: uses: golangci/golangci-lint-action@master with: version: latest - skip-pkg-cache: true - skip-build-cache: true + skip-cache: true + skip-save-cache: true args: --timeout=3m --issues-exit-code=0 ./... - name: Test run: go test -race -v -coverprofile=coverage_temp.out -covermode=atomic ./... - name: Remove mocks and cmd from coverage - run: grep -v -e "/eebus-go/mocks/" -e "/eebus-go/cmd/" coverage_temp.out > coverage.out + run: grep -v -e "/eebus-go/mocks/" -e "/eebus-go/usecases/mocks/" -e "/eebus-go/cmd/" coverage_temp.out > coverage.out - name: Send coverage uses: coverallsapp/github-action@v2 diff --git a/README.md b/README.md index be794342..871a5eba 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ The supported functionality contains: ## Packages - `api`: global API interface definitions and eebus service configuration -- `features`: provides feature helpers with the local SPINE feature having the client role and the remote SPINE feature being the server for easy access to commonly used functions +- `features/client`: provides feature helpers with the local SPINE feature having the client role and the remote SPINE feature being the server for easy access to commonly used functions +- `features/server`: provides feature helpers with the local SPINE feature having the server role for easy access to commonly used functions - `service`: central package which provides access to SHIP and SPINE. Use this to create the EEBUS service, its configuration and connect to remote EEBUS services -- `util`: package with various useful helper functions +- `usecases`: containing actor and use case based implementations with use case scenario based APIs and events ## Usage diff --git a/api/api.go b/api/api.go index 1f06faa2..c4ba94c5 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,9 @@ type ServiceInterface interface { // shutdown the service Shutdown() + // add a use case to the service + AddUseCase(useCase UseCaseInterface) + // set logging interface SetLogging(logger logging.LoggingInterface) diff --git a/api/errors.go b/api/errors.go index 5ef6d889..96eeffb7 100644 --- a/api/errors.go +++ b/api/errors.go @@ -25,3 +25,5 @@ var ErrOperationOnFunctionNotSupported = errors.New("operation is not supported var ErrMissingData = errors.New("missing data") var ErrDeviceDisconnected = errors.New("device is disconnected") + +var ErrNoCompatibleEntity = errors.New("no compatible entity") diff --git a/api/types.go b/api/types.go new file mode 100644 index 00000000..5c4a2121 --- /dev/null +++ b/api/types.go @@ -0,0 +1,4 @@ +package api + +// type for cem and usecase specfic event names +type EventType string diff --git a/api/usecases.go b/api/usecases.go new file mode 100644 index 00000000..ee7333ca --- /dev/null +++ b/api/usecases.go @@ -0,0 +1,51 @@ +package api + +import ( + spineapi "github.com/enbility/spine-go/api" +) + +// Entity event callback +// +// Used by Use Case implementations +type EntityEventCallback func(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event EventType) + +type UseCaseBaseInterface interface { + // add the use case + AddUseCase() + + // update availability of the use case + UpdateUseCaseAvailability(available bool) + + // check if the entity is compatible with the use case + IsCompatibleEntity(entity spineapi.EntityRemoteInterface) bool +} + +// Implemented by each Use Case +type UseCaseInterface interface { + UseCaseBaseInterface + + // add the features + AddFeatures() + + // returns if the entity supports the usecase + // + // possible errors: + // - ErrDataNotAvailable if that information is not (yet) available + // - and others + IsUseCaseSupported(remoteEntity spineapi.EntityRemoteInterface) (bool, error) +} + +type ManufacturerData struct { + DeviceName string `json:"deviceName,omitempty"` + DeviceCode string `json:"deviceCode,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` + SoftwareRevision string `json:"softwareRevision,omitempty"` + HardwareRevision string `json:"hardwareRevision,omitempty"` + VendorName string `json:"vendorName,omitempty"` + VendorCode string `json:"vendorCode,omitempty"` + BrandName string `json:"brandName,omitempty"` + PowerSource string `json:"powerSource,omitempty"` + ManufacturerNodeIdentification string `json:"manufacturerNodeIdentification,omitempty"` + ManufacturerLabel string `json:"manufacturerLabel,omitempty"` + ManufacturerDescription string `json:"manufacturerDescription,omitempty"` +} diff --git a/cmd/evse/main.go b/cmd/evse/main.go index 26f180b9..f0baa7ac 100644 --- a/cmd/evse/main.go +++ b/cmd/evse/main.go @@ -15,6 +15,7 @@ import ( "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/usecases/cs/lpc" shipapi "github.com/enbility/ship-go/api" "github.com/enbility/ship-go/cert" "github.com/enbility/spine-go/model" @@ -82,6 +83,10 @@ func (h *evse) run() { return } + localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeEVSE) + uclpc := lpc.NewCsLPC(localEntity, nil) + h.myService.AddUseCase(uclpc) + if len(remoteSki) == 0 { os.Exit(0) } diff --git a/cmd/hems/main.go b/cmd/hems/main.go index 354805ec..a2b0ed25 100644 --- a/cmd/hems/main.go +++ b/cmd/hems/main.go @@ -15,6 +15,7 @@ import ( "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/service" + "github.com/enbility/eebus-go/usecases/eg/lpc" shipapi "github.com/enbility/ship-go/api" "github.com/enbility/ship-go/cert" "github.com/enbility/spine-go/model" @@ -82,6 +83,10 @@ func (h *hems) run() { return } + localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + uclpc := lpc.NewEgLPC(localEntity, nil) + h.myService.AddUseCase(uclpc) + if len(remoteSki) == 0 { os.Exit(0) } diff --git a/features/client/deviceclassification_test.go b/features/client/deviceclassification_test.go index 24318df0..1a4404a2 100644 --- a/features/client/deviceclassification_test.go +++ b/features/client/deviceclassification_test.go @@ -46,6 +46,10 @@ func (s *DeviceClassificationSuite) BeforeTest(suiteName, testName string) { ) var err error + s.deviceClassification, err = features.NewDeviceClassification(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.deviceClassification) + s.deviceClassification, err = features.NewDeviceClassification(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.deviceClassification) diff --git a/features/client/deviceconfiguration_test.go b/features/client/deviceconfiguration_test.go index 9e728a88..79b9a23d 100644 --- a/features/client/deviceconfiguration_test.go +++ b/features/client/deviceconfiguration_test.go @@ -60,6 +60,10 @@ func (s *DeviceConfigurationSuite) BeforeTest(suiteName, testName string) { mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() var err error + s.deviceConfiguration, err = features.NewDeviceConfiguration(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.deviceConfiguration) + s.deviceConfiguration, err = features.NewDeviceConfiguration(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.deviceConfiguration) diff --git a/features/client/devicediagnosis_test.go b/features/client/devicediagnosis_test.go index 911fb436..b90064bf 100644 --- a/features/client/devicediagnosis_test.go +++ b/features/client/devicediagnosis_test.go @@ -47,6 +47,10 @@ func (s *DeviceDiagnosisSuite) BeforeTest(suiteName, testName string) { ) var err error + s.deviceDiagnosis, err = features.NewDeviceDiagnosis(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.deviceDiagnosis) + s.deviceDiagnosis, err = features.NewDeviceDiagnosis(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.deviceDiagnosis) diff --git a/features/client/electricalconnection_test.go b/features/client/electricalconnection_test.go index 6cb54d2c..cc72459b 100644 --- a/features/client/electricalconnection_test.go +++ b/features/client/electricalconnection_test.go @@ -49,6 +49,10 @@ func (s *ElectricalConnectionSuite) BeforeTest(suiteName, testName string) { ) var err error + s.electricalConnection, err = features.NewElectricalConnection(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.electricalConnection) + s.electricalConnection, err = features.NewElectricalConnection(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.electricalConnection) diff --git a/features/client/feature_test.go b/features/client/feature_test.go index ac851fd9..59115db4 100644 --- a/features/client/feature_test.go +++ b/features/client/feature_test.go @@ -47,6 +47,10 @@ func (s *FeatureSuite) BeforeTest(suiteName, testName string) { ) var err error + s.testFeature, err = features.NewFeature(model.FeatureTypeTypeAlarm, s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.testFeature) + s.testFeature, err = features.NewFeature(model.FeatureTypeTypeAlarm, s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.testFeature) diff --git a/features/client/identification_test.go b/features/client/identification_test.go index 421b8d67..a9ffe3d7 100644 --- a/features/client/identification_test.go +++ b/features/client/identification_test.go @@ -46,6 +46,10 @@ func (s *IdentificationSuite) BeforeTest(suiteName, testName string) { ) var err error + s.identification, err = features.NewIdentification(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.identification) + s.identification, err = features.NewIdentification(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.identification) diff --git a/features/client/incentivetable_test.go b/features/client/incentivetable_test.go index ca5dbb69..6fcf43cb 100644 --- a/features/client/incentivetable_test.go +++ b/features/client/incentivetable_test.go @@ -49,6 +49,10 @@ func (s *IncentiveTableSuite) BeforeTest(suiteName, testName string) { ) var err error + s.incentiveTable, err = features.NewIncentiveTable(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.incentiveTable) + s.incentiveTable, err = features.NewIncentiveTable(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.incentiveTable) diff --git a/features/client/loadcontrol_test.go b/features/client/loadcontrol_test.go index b9d37272..cd41f11c 100644 --- a/features/client/loadcontrol_test.go +++ b/features/client/loadcontrol_test.go @@ -49,6 +49,10 @@ func (s *LoadControlSuite) BeforeTest(suiteName, testName string) { ) var err error + s.loadControl, err = features.NewLoadControl(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.loadControl) + s.loadControl, err = features.NewLoadControl(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.loadControl) diff --git a/features/client/measurement_test.go b/features/client/measurement_test.go index e34372c7..862a6926 100644 --- a/features/client/measurement_test.go +++ b/features/client/measurement_test.go @@ -56,6 +56,10 @@ func (s *MeasurementSuite) BeforeTest(suiteName, testName string) { ) var err error + s.measurement, err = features.NewMeasurement(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.measurement) + s.measurement, err = features.NewMeasurement(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.measurement) diff --git a/features/client/smartenergymanagementps_test.go b/features/client/smartenergymanagementps_test.go index 42586381..7c715d78 100644 --- a/features/client/smartenergymanagementps_test.go +++ b/features/client/smartenergymanagementps_test.go @@ -46,6 +46,10 @@ func (s *SmartEnergyManagementPsSuite) BeforeTest(suiteName, testName string) { ) var err error + s.smartenergymgmtps, err = features.NewSmartEnergyManagementPs(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.smartenergymgmtps) + s.smartenergymgmtps, err = features.NewSmartEnergyManagementPs(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.smartenergymgmtps) diff --git a/features/client/timeseries_test.go b/features/client/timeseries_test.go index 6cdeff90..5c14289b 100644 --- a/features/client/timeseries_test.go +++ b/features/client/timeseries_test.go @@ -49,6 +49,10 @@ func (s *TimeSeriesSuite) BeforeTest(suiteName, testName string) { ) var err error + s.timeSeries, err = features.NewTimeSeries(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.timeSeries) + s.timeSeries, err = features.NewTimeSeries(s.localEntity, s.remoteEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.timeSeries) diff --git a/features/internal/electricalconnection.go b/features/internal/electricalconnection.go index 02eeab00..239d0a87 100644 --- a/features/internal/electricalconnection.go +++ b/features/internal/electricalconnection.go @@ -45,13 +45,11 @@ func (e *ElectricalConnectionCommon) CheckEventPayloadDataForFilter(payloadData if err != nil { return false } - for _, desc := range descs { - if desc.ParameterId == nil { - continue - } + for _, desc := range descs { for _, item := range data.ElectricalConnectionPermittedValueSetData { if item.ParameterId != nil && + desc.ParameterId != nil && *item.ParameterId == *desc.ParameterId && len(item.PermittedValueSet) != 0 { return true diff --git a/features/internal/electricalconnection_test.go b/features/internal/electricalconnection_test.go index 340d3c1d..b7c624bf 100644 --- a/features/internal/electricalconnection_test.go +++ b/features/internal/electricalconnection_test.go @@ -807,6 +807,9 @@ func (s *ElectricalConnectionSuite) Test_EVCurrentLimits() { func (s *ElectricalConnectionSuite) addDescription() { fData := &model.ElectricalConnectionDescriptionListDataType{ ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + PowerSupplyType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeDc), + }, { ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), PowerSupplyType: util.Ptr(model.ElectricalConnectionVoltageTypeTypeAc), diff --git a/features/internal/helper.go b/features/internal/helper.go index e3c1a0a7..98539675 100644 --- a/features/internal/helper.go +++ b/features/internal/helper.go @@ -36,7 +36,8 @@ func searchFilterInItem[T any](item T, filter T) bool { continue } - if !filterField.IsNil() && !itemField.IsNil() && filterField.Elem().Interface() != itemField.Elem().Interface() { + if (!filterField.IsNil() && !itemField.IsNil() && filterField.Elem().Interface() != itemField.Elem().Interface()) || + (!filterField.IsNil() && itemField.IsNil()) { match = false break } diff --git a/features/internal/loadcontrol.go b/features/internal/loadcontrol.go index 11c3eaf2..9b9a9ea2 100644 --- a/features/internal/loadcontrol.go +++ b/features/internal/loadcontrol.go @@ -42,12 +42,9 @@ func (l *LoadControlCommon) CheckEventPayloadDataForFilter(payloadData any, filt descs, _ := l.GetLimitDescriptionsForFilter(filterData) for _, desc := range descs { - if desc.LimitId == nil { - continue - } - for _, item := range data.LoadControlLimitData { if item.LimitId != nil && + desc.LimitId != nil && *item.LimitId == *desc.LimitId && item.Value != nil { return true diff --git a/features/server/deviceconfiguration_test.go b/features/server/deviceconfiguration_test.go index c9a2a09b..77d96133 100644 --- a/features/server/deviceconfiguration_test.go +++ b/features/server/deviceconfiguration_test.go @@ -68,6 +68,9 @@ func (s *DeviceConfigurationSuite) BeforeTest(suiteName, testName string) { s.remoteEntity = entities[1] var err error + s.sut, err = server.NewDeviceConfiguration(nil) + assert.NotNil(s.T(), err) + s.sut, err = server.NewDeviceConfiguration(s.localEntity) assert.Nil(s.T(), err) } diff --git a/features/server/devicediagnosis_test.go b/features/server/devicediagnosis_test.go index 5d46dc47..aac1de9d 100644 --- a/features/server/devicediagnosis_test.go +++ b/features/server/devicediagnosis_test.go @@ -68,6 +68,9 @@ func (s *DeviceDiagnosisSuite) BeforeTest(suiteName, testName string) { s.remoteEntity = entities[1] var err error + s.sut, err = server.NewDeviceDiagnosis(nil) + assert.NotNil(s.T(), err) + s.sut, err = server.NewDeviceDiagnosis(s.localEntity) assert.Nil(s.T(), err) } diff --git a/features/server/electricalconnection_test.go b/features/server/electricalconnection_test.go index 650ae95e..5488c479 100644 --- a/features/server/electricalconnection_test.go +++ b/features/server/electricalconnection_test.go @@ -68,6 +68,9 @@ func (s *ElectricalConnectionSuite) BeforeTest(suiteName, testName string) { s.remoteEntity = entities[1] var err error + s.sut, err = server.NewElectricalConnection(nil) + assert.NotNil(s.T(), err) + s.sut, err = server.NewElectricalConnection(s.localEntity) assert.Nil(s.T(), err) } diff --git a/features/server/feature_test.go b/features/server/feature_test.go index 898e68d7..22b4a0d1 100644 --- a/features/server/feature_test.go +++ b/features/server/feature_test.go @@ -62,6 +62,10 @@ func (s *FeatureSuite) BeforeTest(suiteName, testName string) { s.remoteEntity = entities[1] var err error + s.testFeature, err = features.NewFeature(model.FeatureTypeTypeLoadControl, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.testFeature) + s.testFeature, err = features.NewFeature(model.FeatureTypeTypeLoadControl, s.localEntity) assert.Nil(s.T(), err) assert.NotNil(s.T(), s.testFeature) diff --git a/features/server/loadcontrol_test.go b/features/server/loadcontrol_test.go index 57492f6a..aed2a4ea 100644 --- a/features/server/loadcontrol_test.go +++ b/features/server/loadcontrol_test.go @@ -68,6 +68,9 @@ func (s *LoadControlSuite) BeforeTest(suiteName, testName string) { s.remoteEntity = entities[1] var err error + s.sut, err = server.NewLoadControl(nil) + assert.NotNil(s.T(), err) + s.sut, err = server.NewLoadControl(s.localEntity) assert.Nil(s.T(), err) } diff --git a/features/server/measurement_test.go b/features/server/measurement_test.go index 6f424faf..00fee5f7 100644 --- a/features/server/measurement_test.go +++ b/features/server/measurement_test.go @@ -68,6 +68,9 @@ func (s *MeasurementSuite) BeforeTest(suiteName, testName string) { s.remoteEntity = entities[1] var err error + s.sut, err = server.NewMeasurement(nil) + assert.NotNil(s.T(), err) + s.sut, err = server.NewMeasurement(s.localEntity) assert.Nil(s.T(), err) } @@ -131,8 +134,7 @@ func (s *MeasurementSuite) Test_GetDescriptionsForFilter() { data, err = s.sut.GetDescriptionsForFilter(filter) assert.Nil(s.T(), err) - assert.Equal(s.T(), 1, len(data)) - assert.NotNil(s.T(), data[0].MeasurementId) + assert.Equal(s.T(), 0, len(data)) filter = model.MeasurementDescriptionDataType{ MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), diff --git a/mocks/EntityEventCallback.go b/mocks/EntityEventCallback.go new file mode 100644 index 00000000..ce99f5df --- /dev/null +++ b/mocks/EntityEventCallback.go @@ -0,0 +1,73 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + eebus_goapi "github.com/enbility/eebus-go/api" + api "github.com/enbility/spine-go/api" + + mock "github.com/stretchr/testify/mock" +) + +// EntityEventCallback is an autogenerated mock type for the EntityEventCallback type +type EntityEventCallback struct { + mock.Mock +} + +type EntityEventCallback_Expecter struct { + mock *mock.Mock +} + +func (_m *EntityEventCallback) EXPECT() *EntityEventCallback_Expecter { + return &EntityEventCallback_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: ski, device, entity, event +func (_m *EntityEventCallback) Execute(ski string, device api.DeviceRemoteInterface, entity api.EntityRemoteInterface, event eebus_goapi.EventType) { + _m.Called(ski, device, entity, event) +} + +// EntityEventCallback_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type EntityEventCallback_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - ski string +// - device api.DeviceRemoteInterface +// - entity api.EntityRemoteInterface +// - event eebus_goapi.EventType +func (_e *EntityEventCallback_Expecter) Execute(ski interface{}, device interface{}, entity interface{}, event interface{}) *EntityEventCallback_Execute_Call { + return &EntityEventCallback_Execute_Call{Call: _e.mock.On("Execute", ski, device, entity, event)} +} + +func (_c *EntityEventCallback_Execute_Call) Run(run func(ski string, device api.DeviceRemoteInterface, entity api.EntityRemoteInterface, event eebus_goapi.EventType)) *EntityEventCallback_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(api.DeviceRemoteInterface), args[2].(api.EntityRemoteInterface), args[3].(eebus_goapi.EventType)) + }) + return _c +} + +func (_c *EntityEventCallback_Execute_Call) Return() *EntityEventCallback_Execute_Call { + _c.Call.Return() + return _c +} + +func (_c *EntityEventCallback_Execute_Call) RunAndReturn(run func(string, api.DeviceRemoteInterface, api.EntityRemoteInterface, eebus_goapi.EventType)) *EntityEventCallback_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewEntityEventCallback creates a new instance of EntityEventCallback. 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 NewEntityEventCallback(t interface { + mock.TestingT + Cleanup(func()) +}) *EntityEventCallback { + mock := &EntityEventCallback{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/ServiceInterface.go b/mocks/ServiceInterface.go index ad2bc80f..f6d55a82 100644 --- a/mocks/ServiceInterface.go +++ b/mocks/ServiceInterface.go @@ -26,6 +26,39 @@ func (_m *ServiceInterface) EXPECT() *ServiceInterface_Expecter { return &ServiceInterface_Expecter{mock: &_m.Mock} } +// AddUseCase provides a mock function with given fields: useCase +func (_m *ServiceInterface) AddUseCase(useCase api.UseCaseInterface) { + _m.Called(useCase) +} + +// ServiceInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type ServiceInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +// - useCase api.UseCaseInterface +func (_e *ServiceInterface_Expecter) AddUseCase(useCase interface{}) *ServiceInterface_AddUseCase_Call { + return &ServiceInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase", useCase)} +} + +func (_c *ServiceInterface_AddUseCase_Call) Run(run func(useCase api.UseCaseInterface)) *ServiceInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.UseCaseInterface)) + }) + return _c +} + +func (_c *ServiceInterface_AddUseCase_Call) Return() *ServiceInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *ServiceInterface_AddUseCase_Call) RunAndReturn(run func(api.UseCaseInterface)) *ServiceInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + // CancelPairingWithSKI provides a mock function with given fields: ski func (_m *ServiceInterface) CancelPairingWithSKI(ski string) { _m.Called(ski) diff --git a/mocks/UseCaseBaseInterface.go b/mocks/UseCaseBaseInterface.go new file mode 100644 index 00000000..40fe038c --- /dev/null +++ b/mocks/UseCaseBaseInterface.go @@ -0,0 +1,146 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// UseCaseBaseInterface is an autogenerated mock type for the UseCaseBaseInterface type +type UseCaseBaseInterface struct { + mock.Mock +} + +type UseCaseBaseInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UseCaseBaseInterface) EXPECT() *UseCaseBaseInterface_Expecter { + return &UseCaseBaseInterface_Expecter{mock: &_m.Mock} +} + +// AddUseCase provides a mock function with given fields: +func (_m *UseCaseBaseInterface) AddUseCase() { + _m.Called() +} + +// UseCaseBaseInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UseCaseBaseInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UseCaseBaseInterface_Expecter) AddUseCase() *UseCaseBaseInterface_AddUseCase_Call { + return &UseCaseBaseInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UseCaseBaseInterface_AddUseCase_Call) Run(run func()) *UseCaseBaseInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseBaseInterface_AddUseCase_Call) Return() *UseCaseBaseInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseBaseInterface_AddUseCase_Call) RunAndReturn(run func()) *UseCaseBaseInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *UseCaseBaseInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// UseCaseBaseInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type UseCaseBaseInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *UseCaseBaseInterface_Expecter) IsCompatibleEntity(entity interface{}) *UseCaseBaseInterface_IsCompatibleEntity_Call { + return &UseCaseBaseInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *UseCaseBaseInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *UseCaseBaseInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UseCaseBaseInterface_IsCompatibleEntity_Call) Return(_a0 bool) *UseCaseBaseInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UseCaseBaseInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *UseCaseBaseInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UseCaseBaseInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UseCaseBaseInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UseCaseBaseInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UseCaseBaseInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UseCaseBaseInterface_UpdateUseCaseAvailability_Call { + return &UseCaseBaseInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UseCaseBaseInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UseCaseBaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UseCaseBaseInterface_UpdateUseCaseAvailability_Call) Return() *UseCaseBaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseBaseInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UseCaseBaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewUseCaseBaseInterface creates a new instance of UseCaseBaseInterface. 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 NewUseCaseBaseInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UseCaseBaseInterface { + mock := &UseCaseBaseInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/UseCaseInterface.go b/mocks/UseCaseInterface.go new file mode 100644 index 00000000..e85dee18 --- /dev/null +++ b/mocks/UseCaseInterface.go @@ -0,0 +1,234 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// UseCaseInterface is an autogenerated mock type for the UseCaseInterface type +type UseCaseInterface struct { + mock.Mock +} + +type UseCaseInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *UseCaseInterface) EXPECT() *UseCaseInterface_Expecter { + return &UseCaseInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *UseCaseInterface) AddFeatures() { + _m.Called() +} + +// UseCaseInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type UseCaseInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *UseCaseInterface_Expecter) AddFeatures() *UseCaseInterface_AddFeatures_Call { + return &UseCaseInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *UseCaseInterface_AddFeatures_Call) Run(run func()) *UseCaseInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseInterface_AddFeatures_Call) Return() *UseCaseInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseInterface_AddFeatures_Call) RunAndReturn(run func()) *UseCaseInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *UseCaseInterface) AddUseCase() { + _m.Called() +} + +// UseCaseInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type UseCaseInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *UseCaseInterface_Expecter) AddUseCase() *UseCaseInterface_AddUseCase_Call { + return &UseCaseInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *UseCaseInterface_AddUseCase_Call) Run(run func()) *UseCaseInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *UseCaseInterface_AddUseCase_Call) Return() *UseCaseInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseInterface_AddUseCase_Call) RunAndReturn(run func()) *UseCaseInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *UseCaseInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// UseCaseInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type UseCaseInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *UseCaseInterface_Expecter) IsCompatibleEntity(entity interface{}) *UseCaseInterface_IsCompatibleEntity_Call { + return &UseCaseInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *UseCaseInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *UseCaseInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UseCaseInterface_IsCompatibleEntity_Call) Return(_a0 bool) *UseCaseInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *UseCaseInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *UseCaseInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *UseCaseInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UseCaseInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type UseCaseInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *UseCaseInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *UseCaseInterface_IsUseCaseSupported_Call { + return &UseCaseInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *UseCaseInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *UseCaseInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *UseCaseInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *UseCaseInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *UseCaseInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *UseCaseInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *UseCaseInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// UseCaseInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type UseCaseInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *UseCaseInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *UseCaseInterface_UpdateUseCaseAvailability_Call { + return &UseCaseInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *UseCaseInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *UseCaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *UseCaseInterface_UpdateUseCaseAvailability_Call) Return() *UseCaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *UseCaseInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *UseCaseInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewUseCaseInterface creates a new instance of UseCaseInterface. 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 NewUseCaseInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *UseCaseInterface { + mock := &UseCaseInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/service/service.go b/service/service.go index e700b3ae..100dce95 100644 --- a/service/service.go +++ b/service/service.go @@ -33,6 +33,8 @@ type Service struct { serviceHandler api.ServiceReaderInterface + usecases []api.UseCaseInterface + // defines wether a user interaction to accept pairing is possible isPairingPossible bool @@ -148,6 +150,14 @@ func (s *Service) Shutdown() { s.connectionsHub.Shutdown() } +// add a use case to the service +func (s *Service) AddUseCase(useCase api.UseCaseInterface) { + s.usecases = append(s.usecases, useCase) + + useCase.AddFeatures() + useCase.AddUseCase() +} + func (s *Service) Configuration() *api.Configuration { return s.configuration } diff --git a/service/service_test.go b/service/service_test.go index dbccf1ce..1da17d76 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -56,6 +56,14 @@ func (s *ServiceSuite) BeforeTest(suiteName, testName string) { s.sut = NewService(s.config, s.serviceReader) } +func (s *ServiceSuite) Test_AddUseCase() { + ucMock := mocks.NewUseCaseInterface(s.T()) + ucMock.EXPECT().AddFeatures().Return().Once() + ucMock.EXPECT().AddUseCase().Return().Once() + + s.sut.AddUseCase(ucMock) +} + func (s *ServiceSuite) Test_EEBUSHandler() { testSki := "test" diff --git a/usecases/README.md b/usecases/README.md new file mode 100644 index 00000000..230ee146 --- /dev/null +++ b/usecases/README.md @@ -0,0 +1,40 @@ +# Use Cases + +This folder contains various use case implementations for different use case actors. Each use case provides a scenario based API and messages for event / data updates. + +Actors: + +- `cem`: Customer Energy Management + + Use Cases: + - `cevc`: Coordinated EV Charging + - `evcc`: EV Commissioning and Configuration + - `evcem`: EV Charging Electricity Measurement + - `evsecc`: EVSE Commissioning and Configuration + - `evsoc`: EV State Of Charge + - `opev`: Overload Protection by EV Charging Current Curtailment + - `oscev`: Optimization of Self-Consumption During EV Charging + - `vabd`: Visualization of Aggregated Battery Data + - `vapd`: Visualization of Aggregated Photovoltaic Data + +- `cs`: Controllable System + + Use Cases: + - `lpc`: Limitation of Power Consumption + - `lpp`: Limitation of Power Production + +- `eg`: Energy Guard + + Use Cases: + - `lpc`: Limitation of Power Consumption + - `lpp`: Limitation of Power Production + +- `gcp`: Grid Connection Point + + Use Cases: + - `mgcp`: Monitoring of Grid Connection Point + +- `ma`: Monitoring Appliance + + Use Cases: + - `mpc`: Monitoring of Power Consumption diff --git a/usecases/api/api.go b/usecases/api/api.go new file mode 100644 index 00000000..a605d806 --- /dev/null +++ b/usecases/api/api.go @@ -0,0 +1,3 @@ +package api + +//go:generate mockery diff --git a/usecases/api/cem_cevc.go b/usecases/api/cem_cevc.go new file mode 100644 index 00000000..13c18624 --- /dev/null +++ b/usecases/api/cem_cevc.go @@ -0,0 +1,91 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Customer Energy Management +// UseCase: Coordinated EV Charging +type CemCEVCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // returns the current charging stratey + // + // parameters: + // - entity: the entity of the EV + // + // returns EVChargeStrategyTypeUnknown if it could not be determined, e.g. + // if the vehicle communication is via IEC61851 or the EV doesn't provide + // any information about its charging mode or plan + ChargeStrategy(remoteEntity spineapi.EntityRemoteInterface) EVChargeStrategyType + + // returns the current energy demand + // + // parameters: + // - entity: the entity of the EV + // + // return values: + // - EVDemand: details about the actual demands from the EV + // - error: if no data is available + // + // if duration is 0, direct charging is active, otherwise timed charging is active + EnergyDemand(remoteEntity spineapi.EntityRemoteInterface) (Demand, error) + + // Scenario 2 + + TimeSlotConstraints(entity spineapi.EntityRemoteInterface) (TimeSlotConstraints, error) + + // send power limits to the EV + // + // parameters: + // - entity: the entity of the EV + // - data: the power limits + // + // if no data is provided, default power limits with the max possible value for 7 days will be sent + WritePowerLimits(entity spineapi.EntityRemoteInterface, data []DurationSlotValue) error + + // Scenario 3 + + // return the current incentive constraints + // + // parameters: + // - entity: the entity of the EV + IncentiveConstraints(entity spineapi.EntityRemoteInterface) (IncentiveSlotConstraints, error) + + // send new incentives to the EV + // + // parameters: + // - entity: the entity of the EV + // - data: the incentive descriptions + WriteIncentiveTableDescriptions(entity spineapi.EntityRemoteInterface, data []IncentiveTariffDescription) error + + // send incentives to the EV + // + // parameters: + // - entity: the entity of the EV + // - data: the incentives + // + // if no data is provided, default incentives with the same price for 7 days will be sent + WriteIncentives(entity spineapi.EntityRemoteInterface, data []DurationSlotValue) error + + // Scenario 4 + + // return the current charge plan constraints + // + // parameters: + // - entity: the entity of the EV + ChargePlanConstraints(entity spineapi.EntityRemoteInterface) ([]DurationSlotValue, error) + + // return the current charge plan of the EV + // + // parameters: + // - entity: the entity of the EV + ChargePlan(entity spineapi.EntityRemoteInterface) (ChargePlan, error) + + // Scenario 5 & 6 + + // this is automatically covered by the SPINE implementation +} diff --git a/usecases/api/cem_evcc.go b/usecases/api/cem_evcc.go new file mode 100644 index 00000000..f8b3fdb0 --- /dev/null +++ b/usecases/api/cem_evcc.go @@ -0,0 +1,77 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Customer Energy Management +// UseCase: EV Commissioning and Configuration +type CemEVCCInterface interface { + api.UseCaseInterface + + // return the current charge state of the EV + // + // parameters: + // - entity: the entity of the EV + ChargeState(entity spineapi.EntityRemoteInterface) (EVChargeStateType, error) + + // Scenario 1 & 8 + + // return if the EV is connected + // + // parameters: + // - entity: the entity of the EV + EVConnected(entity spineapi.EntityRemoteInterface) bool + + // Scenario 2 + + // return the current communication standard type used to communicate between EVSE and EV + // + // parameters: + // - entity: the entity of the EV + CommunicationStandard(entity spineapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error) + + // Scenario 3 + + // return if the EV supports asymmetric charging + // + // parameters: + // - entity: the entity of the EV + AsymmetricChargingSupport(entity spineapi.EntityRemoteInterface) (bool, error) + + // Scenario 4 + + // return the identifications of the currently connected EV or nil if not available + // these can be multiple, e.g. PCID, Mac Address, RFID + // + // parameters: + // - entity: the entity of the EV + Identifications(entity spineapi.EntityRemoteInterface) ([]IdentificationItem, error) + + // Scenario 5 + + // the manufacturer data of an EVSE + // returns deviceName, serialNumber, error + // + // parameters: + // - entity: the entity of the EV + ManufacturerData(entity spineapi.EntityRemoteInterface) (api.ManufacturerData, error) + + // Scenario 6 + + // return the minimum, maximum charging and, standby power of the connected EV + // + // parameters: + // - entity: the entity of the EV + ChargingPowerLimits(entity spineapi.EntityRemoteInterface) (float64, float64, float64, error) + + // Scenario 7 + + // is the EV in sleep mode + // + // parameters: + // - entity: the entity of the EV + IsInSleepMode(entity spineapi.EntityRemoteInterface) (bool, error) +} diff --git a/usecases/api/cem_evcem.go b/usecases/api/cem_evcem.go new file mode 100644 index 00000000..af7d00d7 --- /dev/null +++ b/usecases/api/cem_evcem.go @@ -0,0 +1,42 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Customer Energy Management +// UseCase: EV Charging Electricity Measurement +type CemEVCEMInterface interface { + api.UseCaseInterface + + // return the number of ac connected phases of the EV or 0 if it is unknown + // + // parameters: + // - entity: the entity of the EV + PhasesConnected(entity spineapi.EntityRemoteInterface) (uint, error) + + // Scenario 1 + + // return the last current measurement for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 2 + + // return the last power measurement for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 3 + + // return the charged energy measurement in Wh of the connected EV + // + // parameters: + // - entity: the entity of the EV + EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/api/cem_evsecc.go b/usecases/api/cem_evsecc.go new file mode 100644 index 00000000..1d7aaf3d --- /dev/null +++ b/usecases/api/cem_evsecc.go @@ -0,0 +1,29 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Customer Energy Management +// UseCase: EVSE Commissioning and Configuration +type CemEVSECCInterface interface { + api.UseCaseInterface + + // the manufacturer data of an EVSE + // + // parameters: + // - entity: the entity of the EV + // + // returns deviceName, serialNumber, error + ManufacturerData(entity spineapi.EntityRemoteInterface) (api.ManufacturerData, error) + + // the operating state data of an EVSE + // + // parameters: + // - entity: the entity of the EV + // + // returns operatingState, lastErrorCode, error + OperatingState(entity spineapi.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error) +} diff --git a/usecases/api/cem_evsoc.go b/usecases/api/cem_evsoc.go new file mode 100644 index 00000000..7ccc2fda --- /dev/null +++ b/usecases/api/cem_evsoc.go @@ -0,0 +1,22 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Customer Energy Management +// UseCase: EV State Of Charge +type CemEVSOCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the EVscurrent state of charge of the EV or an error it is unknown + // + // parameters: + // - entity: the entity of the EV + StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 to 4 are not supported, as there is no EV supporting this as of today +} diff --git a/usecases/api/cem_opev.go b/usecases/api/cem_opev.go new file mode 100644 index 00000000..4b34b665 --- /dev/null +++ b/usecases/api/cem_opev.go @@ -0,0 +1,64 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Customer Energy Management +// UseCase: Overload Protection by EV Charging Current Curtailment +type CemOPEVInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the min, max, default limits for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) + + // return the current loadcontrol obligation limits + // + // parameters: + // - entity: the entity of the EV + // + // return values: + // - limits: per phase data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + LoadControlLimits(entity spineapi.EntityRemoteInterface) (limits []LoadLimitsPhase, resultErr error) + + // send new LoadControlLimits to the remote EV + // + // parameters: + // - entity: the entity of the EV + // - limits: a set of limits containing phase specific limit data + // + // Sets a maximum A limit for each phase that the EV may not exceed. + // Mainly used for implementing overload protection of the site or limiting the + // maximum charge power of EVs when the EV and EVSE communicate via IEC61851 + // and with ISO15118 if the EV does not support the Optimization of Self Consumption + // usecase. + // + // note: + // For obligations to work for optimizing solar excess power, the EV needs to + // have an energy demand. Recommendations work even if the EV does not have an active + // energy demand, given it communicated with the EVSE via ISO15118 and supports the usecase. + // In ISO15118-2 the usecase is only supported via VAS extensions which are vendor specific + // and needs to have specific EVSE support for the specific EV brand. + // In ISO15118-20 this is a standard feature which does not need special support on the EVSE. + WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []LoadLimitsPhase) (*model.MsgCounterType, error) + + // Scenario 2 + + // this is automatically covered by the SPINE implementation + + // Scenario 3 + + // this is covered by the central CEM interface implementation + // use that one to set the CEM's operation state which will inform all remote devices +} diff --git a/usecases/api/cem_oscev.go b/usecases/api/cem_oscev.go new file mode 100644 index 00000000..2b945835 --- /dev/null +++ b/usecases/api/cem_oscev.go @@ -0,0 +1,57 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Customer Energy Management +// UseCase: Optimization of Self-Consumption During EV Charging +type CemOSCEVInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the min, max, default limits for each phase of the connected EV + // + // parameters: + // - entity: the entity of the EV + CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) + + // return the current loadcontrol recommendation limits + // + // parameters: + // - entity: the entity of the EV + // + // return values: + // - limits: per phase data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + LoadControlLimits(entity spineapi.EntityRemoteInterface) (limits []LoadLimitsPhase, resultErr error) + + // send new LoadControlLimits to the remote EV + // + // parameters: + // - entity: the entity of the EV + // - limits: a set of limits containing phase specific limit data + // + // recommendations: + // Sets a recommended charge power in A for each phase. This is mainly + // used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. + // The EV either needs to support the Optimization of Self Consumption usecase or + // the EVSE needs to be able map the recommendations into oligation limits which then + // works for all EVs communication either via IEC61851 or ISO15118. + WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []LoadLimitsPhase) (*model.MsgCounterType, error) + + // Scenario 2 + + // this is automatically covered by the SPINE implementation + + // Scenario 3 + + // this is covered by the central CEM interface implementation + // use that one to set the CEM's operation state which will inform all remote devices +} diff --git a/usecases/api/cem_vabd.go b/usecases/api/cem_vabd.go new file mode 100644 index 00000000..41d44b6b --- /dev/null +++ b/usecases/api/cem_vabd.go @@ -0,0 +1,44 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Customer Energy Management +// UseCase: Visualization of Aggregated Battery Data +type CemVABDInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current (dis)charging power + // + // parameters: + // - entity: the entity of the inverter + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 + + // return the cumulated battery system charge energy + // + // parameters: + // - entity: the entity of the inverter + EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return the cumulated battery system discharge energy + // + // parameters: + // - entity: the entity of the inverter + EnergyDischarged(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 4 + + // return the current state of charge of the battery system + // + // parameters: + // - entity: the entity of the inverter + StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/api/cem_vapd.go b/usecases/api/cem_vapd.go new file mode 100644 index 00000000..841615c0 --- /dev/null +++ b/usecases/api/cem_vapd.go @@ -0,0 +1,36 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Customer Energy Management +// UseCase: Visualization of Aggregated Photovoltaic Data +type CemVAPDInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current production power + // + // parameters: + // - entity: the entity of the inverter + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 + + // return the nominal peak power + // + // parameters: + // - entity: the entity of the inverter + PowerNominalPeak(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return total PV yield + // + // parameters: + // - entity: the entity of the inverter + PVYieldTotal(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/api/cs_lpc.go b/usecases/api/cs_lpc.go new file mode 100644 index 00000000..544294a7 --- /dev/null +++ b/usecases/api/cs_lpc.go @@ -0,0 +1,94 @@ +package api + +import ( + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Controllable System +// UseCase: Limitation of Power Consumption +type CsLPCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current consumption limit data + // + // return values: + // - limit: load limit data + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + ConsumptionLimit() (LoadLimit, error) + + // set the current loadcontrol limit data + SetConsumptionLimit(limit LoadLimit) (resultErr error) + + // return the currently pending incoming consumption write limits + PendingConsumptionLimits() map[model.MsgCounterType]LoadLimit + + // accept or deny an incoming consumption write limit + // + // parameters: + // - msg: the incoming write message + // - approve: if the write limit for msg should be approved or not + // - reason: the reason why the approval is denied, otherwise an empty string + ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + + // Scenario 2 + + // return Failsafe limit for the consumed 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 + FailsafeConsumptionActivePowerLimit() (value float64, isChangeable bool, resultErr error) + + // set Failsafe limit for the consumed 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 + SetFailsafeConsumptionActivePowerLimit(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 + // + // returns true, if the last heartbeat is within 2 minutes, otherwise false + IsHeartbeatWithinDuration() bool + + // Scenario 4 + + // return nominal maximum active (real) power the Controllable System is + // allowed to consume due to the customer's contract. + ContractualConsumptionNominalMax() (float64, error) + + // set nominal maximum active (real) power the Controllable System is + // allowed to consume due to the customer's contract. + // + // parameters: + // - value: contractual nominal max power consumption in W + SetContractualConsumptionNominalMax(value float64) (resultErr error) +} diff --git a/usecases/api/cs_lpp.go b/usecases/api/cs_lpp.go new file mode 100644 index 00000000..d7c9b5ff --- /dev/null +++ b/usecases/api/cs_lpp.go @@ -0,0 +1,94 @@ +package api + +import ( + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Controllable System +// UseCase: Limitation of Power Production +type CsLPPInterface 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() (LoadLimit, error) + + // set the current loadcontrol limit data + SetProductionLimit(limit LoadLimit) (resultErr error) + + // return the currently pending incoming consumption write limits + PendingProductionLimits() map[model.MsgCounterType]LoadLimit + + // accept or deny an incoming consumption write limit + // + // parameters: + // - msg: the incoming write message + // - approve: if the write limit for msg should be approved or not + // - reason: the reason why the approval is denied, otherwise an empty string + ApproveOrDenyProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + + // 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 + // + // returns true, if the last heartbeat is within 2 minutes, otherwise false + IsHeartbeatWithinDuration() bool + + // 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/usecases/api/eg_lpc.go b/usecases/api/eg_lpc.go new file mode 100644 index 00000000..978e9b9f --- /dev/null +++ b/usecases/api/eg_lpc.go @@ -0,0 +1,86 @@ +package api + +import ( + "time" + + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Energy Guard +// UseCase: Limitation of Power Consumption +type EgLPCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current consumption 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 + ConsumptionLimit(entity spineapi.EntityRemoteInterface) (limit LoadLimit, resultErr error) + + // send new LoadControlLimits + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - limit: load limit data + WriteConsumptionLimit(entity spineapi.EntityRemoteInterface, limit LoadLimit) (*model.MsgCounterType, error) + + // Scenario 2 + + // return Failsafe limit for the consumed 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 consumption + FailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) + + // send new Failsafe Consumption Active Power Limit + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - value: the new limit in W + WriteFailsafeConsumptionActivePowerLimit(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 consume according to the device label or data sheet. + // + // parameters: + // - entity: the entity of the e.g. EVSE + PowerConsumptionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/api/eg_lpp.go b/usecases/api/eg_lpp.go new file mode 100644 index 00000000..a59ca387 --- /dev/null +++ b/usecases/api/eg_lpp.go @@ -0,0 +1,86 @@ +package api + +import ( + "time" + + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Energy Guard +// UseCase: Limitation of Power Production +type EgLPPInterface 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 LoadLimit, resultErr error) + + // send new LoadControlLimits + // + // parameters: + // - entity: the entity of the e.g. EVSE + // - limit: load limit data + WriteProductionLimit(entity spineapi.EntityRemoteInterface, limit 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/usecases/api/gcp_mgcp.go b/usecases/api/gcp_mgcp.go new file mode 100644 index 00000000..6a351b0d --- /dev/null +++ b/usecases/api/gcp_mgcp.go @@ -0,0 +1,86 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Grid Connection Point +// UseCase: Monitoring of Grid Connection Point +type GcpMGCPInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current power limitation factor + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + PowerLimitationFactor(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 2 + + // return the momentary power consumption or production at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - positive values are used for consumption + // - negative values are used for production + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return the total feed in energy at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - negative values are used for production + EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 4 + + // return the total consumption energy at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - positive values are used for consumption + EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 5 + + // return the momentary current consumption or production at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + // + // return values: + // - positive values are used for consumption + // - negative values are used for production + CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 6 + + // return the voltage phase details at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 7 + + // return frequency at the grid connection point + // + // parameters: + // - entity: the entity of the device (e.g. SMGW) + Frequency(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/api/ma_mpc.go b/usecases/api/ma_mpc.go new file mode 100644 index 00000000..57402cb2 --- /dev/null +++ b/usecases/api/ma_mpc.go @@ -0,0 +1,81 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" +) + +// Actor: Monitoring Appliance +// UseCase: Monitoring of Power Consumption +type MaMPCInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the momentary active power consumption or production + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + Power(entity spineapi.EntityRemoteInterface) (float64, error) + + // return the momentary active phase specific power consumption or production per phase + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 2 + + // return the total consumption energy + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // - positive values are used for consumption + EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) + + // return the total feed in energy + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // return values: + // - negative values are used for production + EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, error) + + // Scenario 3 + + // return the momentary phase specific current consumption or production + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + // + // return values + // - positive values are used for consumption + // - negative values are used for production + CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 4 + + // return the phase specific voltage details + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + + // Scenario 5 + + // return frequency + // + // parameters: + // - entity: the entity of the device (e.g. EVSE) + Frequency(entity spineapi.EntityRemoteInterface) (float64, error) +} diff --git a/usecases/api/types.go b/usecases/api/types.go new file mode 100644 index 00000000..ecd11119 --- /dev/null +++ b/usecases/api/types.go @@ -0,0 +1,151 @@ +package api + +import ( + "time" + + "github.com/enbility/spine-go/model" +) + +type EVChargeStateType string + +const ( + EVChargeStateTypeUnknown EVChargeStateType = "Unknown" + EVChargeStateTypeUnplugged EVChargeStateType = "unplugged" + EVChargeStateTypeError EVChargeStateType = "error" + EVChargeStateTypePaused EVChargeStateType = "paused" + EVChargeStateTypeActive EVChargeStateType = "active" + EVChargeStateTypeFinished EVChargeStateType = "finished" +) + +// Defines a phase specific limit data set +type LoadLimitsPhase struct { + Phase model.ElectricalConnectionPhaseNameType // the phase + IsChangeable bool // if the value can be changed via write, ignored when writing data + IsActive bool // if the limit is active + Value float64 // the limit in A +} + +// Defines a limit data set +type LoadLimit struct { + Duration time.Duration // the duration of the limit, + IsChangeable bool // if the value can be changed via write, ignored when writing data + IsActive bool // if the limit is active + Value float64 // the limit in A +} + +// identification +type IdentificationItem struct { + // the identification value + Value string + + // the type of the identification value, e.g. + ValueType model.IdentificationTypeType +} + +type EVChargeStrategyType string + +const ( + EVChargeStrategyTypeUnknown EVChargeStrategyType = "unknown" + EVChargeStrategyTypeNoDemand EVChargeStrategyType = "nodemand" + EVChargeStrategyTypeDirectCharging EVChargeStrategyType = "directcharging" + EVChargeStrategyTypeMinSoC EVChargeStrategyType = "minsoc" + EVChargeStrategyTypeTimedCharging EVChargeStrategyType = "timedcharging" +) + +// Contains details about the actual demands from the EV +// +// General: +// - If duration and energy is 0, charge mode is EVChargeStrategyTypeNoDemand +// - If duration is 0, charge mode is EVChargeStrategyTypeDirectCharging and the slots should cover at least 48h +// - If both are != 0, charge mode is EVChargeStrategyTypeTimedCharging and the slots should cover at least the duration, but at max 168h (7d) +type Demand struct { + MinDemand float64 // minimum demand in Wh to reach the minSoC setting, 0 if not set + OptDemand float64 // demand in Wh to reach the timer SoC setting + MaxDemand float64 // the maximum possible demand until the battery is full + DurationUntilStart float64 // the duration in s from now until charging will start, this could be in the future but usualy is now + DurationUntilEnd float64 // the duration in s from now until minDemand or optDemand has to be reached, 0 if direct charge strategy is active +} + +// Contains details about an EV generated charging plan +type ChargePlan struct { + Slots []ChargePlanSlotValue // Individual charging slot details +} + +// Contains details about a charging plan slot +type ChargePlanSlotValue struct { + Start time.Time // The start time of the slot + End time.Time // The duration of the slot + Value float64 // planned power value + MinValue float64 // minimum power value + MaxValue float64 // maximum power value +} + +// Details about the time slot constraints +type TimeSlotConstraints struct { + MinSlots uint // the minimum number of slots, no minimum if 0 + MaxSlots uint // the maximum number of slots, unlimited if 0 + MinSlotDuration time.Duration // the minimum duration of a slot, no minimum if 0 + MaxSlotDuration time.Duration // the maximum duration of a slot, unlimited if 0 + SlotDurationStepSize time.Duration // the duration has to be a multiple of this value if != 0 +} + +// Details about the incentive slot constraints +type IncentiveSlotConstraints struct { + MinSlots uint // the minimum number of slots, no minimum if 0 + MaxSlots uint // the maximum number of slots, unlimited if 0 +} + +// details about the boundary +type TierBoundaryDescription struct { + // the id of the boundary + Id uint + + // the type of the boundary + Type model.TierBoundaryTypeType + + // the unit of the boundary + Unit model.UnitOfMeasurementType +} + +// details about incentive +type IncentiveDescription struct { + // the id of the incentive + Id uint + + // the type of the incentive + Type model.IncentiveTypeType + + // the currency of the incentive, if it is price based + Currency model.CurrencyType +} + +// Contains about one tier in a tariff +type IncentiveTableDescriptionTier struct { + // the id of the tier + Id uint + + // the tiers type + Type model.TierTypeType + + // each tear has 1 to 3 boundaries + // used for different power limits, e.g. 0-1kW x€, 1-3kW y€, ... + Boundaries []TierBoundaryDescription + + // each tier has 1 to 3 incentives + // - price/costs (absolute or relative) + // - renewable energy percentage + // - CO2 emissions + Incentives []IncentiveDescription +} + +// Contains details about a tariff +type IncentiveTariffDescription struct { + // each tariff can have 1 to 3 tiers + Tiers []IncentiveTableDescriptionTier +} + +// Contains details about power limits or incentives for a defined timeframe +type DurationSlotValue struct { + Duration time.Duration // Duration of this slot + Value float64 // Energy Cost or Power Limit +} diff --git a/usecases/cem/cevc/events.go b/usecases/cem/cevc/events.go new file mode 100644 index 00000000..1a6709c1 --- /dev/null +++ b/usecases/cem/cevc/events.go @@ -0,0 +1,221 @@ +package cevc + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemCEVC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange { + return + } + + if payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.TimeSeriesDescriptionListDataType: + e.evTimeSeriesDescriptionDataUpdate(payload) + + case *model.TimeSeriesListDataType: + e.evTimeSeriesDataUpdate(payload) + + case *model.IncentiveTableDescriptionDataType: + e.evIncentiveTableDescriptionDataUpdate(payload) + + case *model.IncentiveTableConstraintsDataType: + e.evIncentiveTableConstraintsDataUpdate(payload) + + case *model.IncentiveDataType: + e.evIncentiveTableDataUpdate(payload) + } +} + +// an EV was connected +func (e *CemCEVC) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if evDeviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity); err == nil { + if _, err := evDeviceConfiguration.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get device configuration descriptions + if _, err := evDeviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity); err == nil { + if _, err := evTimeSeries.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := evTimeSeries.Bind(); err != nil { + logging.Log().Debug(err) + } + + // get time series descriptions + if _, err := evTimeSeries.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get time series constraints + if _, err := evTimeSeries.RequestConstraints(); err != nil { + logging.Log().Debug(err) + } + } + + if evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, entity); err == nil { + if _, err := evIncentiveTable.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := evIncentiveTable.Bind(); err != nil { + logging.Log().Debug(err) + } + + // get incentivetable descriptions + if _, err := evIncentiveTable.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the time series description data of an EV was updated +func (e *CemCEVC) evTimeSeriesDescriptionDataUpdate(payload spineapi.EventPayload) { + if evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, payload.Entity); err == nil { + // get time series values + if _, err := evTimeSeries.RequestData(); err != nil { + logging.Log().Debug(err) + } + } + + // check if we are required to update the plan + if !e.evCheckTimeSeriesDescriptionConstraintsUpdateRequired(payload.Entity) { + return + } + + _, err := e.EnergyDemand(payload.Entity) + if err != nil { + return + } + + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyDemand) + + _, err = e.TimeSlotConstraints(payload.Entity) + if err != nil { + logging.Log().Error("Error getting timeseries constraints:", err) + return + } + + _, err = e.IncentiveConstraints(payload.Entity) + if err != nil { + logging.Log().Error("Error getting incentive constraints:", err) + return + } + + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataRequestedPowerLimitsAndIncentives) +} + +// the load control limit data of an EV was updated +func (e *CemCEVC) evTimeSeriesDataUpdate(payload spineapi.EventPayload) { + if _, err := e.ChargePlan(payload.Entity); err == nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateChargePlan) + } + + if _, err := e.ChargePlanConstraints(payload.Entity); err == nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateTimeSlotConstraints) + } +} + +// the incentive table description data of an EV was updated +func (e *CemCEVC) evIncentiveTableDescriptionDataUpdate(payload spineapi.EventPayload) { + if evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, payload.Entity); err == nil { + // get time series values + if _, err := evIncentiveTable.RequestValues(); err != nil { + logging.Log().Debug(err) + } + } + + // check if we are required to update the plan + if e.evCheckIncentiveTableDescriptionUpdateRequired(payload.Entity) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataRequestedIncentiveTableDescription) + } +} + +// the incentive table constraint data of an EV was updated +func (e *CemCEVC) evIncentiveTableConstraintsDataUpdate(payload spineapi.EventPayload) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIncentiveTable) +} + +// the incentive table data of an EV was updated +func (e *CemCEVC) evIncentiveTableDataUpdate(payload spineapi.EventPayload) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIncentiveTable) +} + +// check timeSeries descriptions if constraints element has updateRequired set to true +// as this triggers the CEM to send power tables within 20s +func (e *CemCEVC) evCheckTimeSeriesDescriptionConstraintsUpdateRequired(entity spineapi.EntityRemoteInterface) bool { + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + logging.Log().Error("timeseries feature not found") + return false + } + + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + } + data, err := evTimeSeries.GetDescriptionsForFilter(filter) + if err != nil || len(data) == 0 { + return false + } + + if data[0].UpdateRequired != nil { + return *data[0].UpdateRequired + } + + return false +} + +// check incentibeTable descriptions if the tariff description has updateRequired set to true +// as this triggers the CEM to send incentive tables within 20s +func (e *CemCEVC) evCheckIncentiveTableDescriptionUpdateRequired(entity spineapi.EntityRemoteInterface) bool { + evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, entity) + if err != nil { + logging.Log().Error("incentivetable feature not found") + return false + } + + filter := model.TariffDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + } + data, err := evIncentiveTable.GetDescriptionsForFilter(filter) + if err != nil || len(data) == 0 { + return false + } + + // only use the first description and therein the first tariff + item := data[0].TariffDescription + if item != nil && item.UpdateRequired != nil { + return *item.UpdateRequired + } + + return false +} diff --git a/usecases/cem/cevc/events_test.go b/usecases/cem/cevc/events_test.go new file mode 100644 index 00000000..eca0e9f7 --- /dev/null +++ b/usecases/cem/cevc/events_test.go @@ -0,0 +1,169 @@ +package cevc + +import ( + "time" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + 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 = util.Ptr(model.TimeSeriesDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.TimeSeriesListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.IncentiveTableDescriptionDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.IncentiveTableConstraintsDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.IncentiveDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *CEVCSuite) Test_Failures() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.evConnected(s.mockRemoteEntity) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + + s.sut.evTimeSeriesDataUpdate(payload) + + s.sut.evIncentiveTableDescriptionDataUpdate(payload) + + s.sut.evCheckTimeSeriesDescriptionConstraintsUpdateRequired(s.mockRemoteEntity) + + s.sut.evCheckIncentiveTableDescriptionUpdateRequired(s.mockRemoteEntity) +} + +func (s *CEVCSuite) Test_evTimeSeriesDescriptionDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + timeDesc := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + UpdateRequired: util.Ptr(true), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDesc, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + MinValue: model.NewScaledNumberType(1000), + Value: model.NewScaledNumberType(10000), + MaxValue: model.NewScaledNumberType(100000), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err := s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1000.0, demand.MinDemand) + assert.Equal(s.T(), 10000.0, demand.OptDemand) + assert.Equal(s.T(), 100000.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + assert.True(s.T(), s.eventCalled) + s.eventCalled = false + + constData := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: util.Ptr(model.TimeSeriesSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(10)), + SlotDurationMin: model.NewDurationType(1 * time.Minute), + SlotDurationMax: model.NewDurationType(60 * time.Minute), + SlotDurationStepSize: model.NewDurationType(1 * time.Minute), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + assert.True(s.T(), s.eventCalled) + s.eventCalled = false + + incConstData := &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, incConstData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evTimeSeriesDescriptionDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/cevc/public_scen1.go b/usecases/cem/cevc/public_scen1.go new file mode 100644 index 00000000..afe8af1b --- /dev/null +++ b/usecases/cem/cevc/public_scen1.go @@ -0,0 +1,141 @@ +package cevc + +import ( + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// returns the current charging strategy +func (e *CemCEVC) ChargeStrategy(entity spineapi.EntityRemoteInterface) ucapi.EVChargeStrategyType { + if !e.IsCompatibleEntity(entity) { + return ucapi.EVChargeStrategyTypeUnknown + } + + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return ucapi.EVChargeStrategyTypeUnknown + } + + // only the time series data for singledemand is relevant for detecting the charging strategy + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + } + data, err := evTimeSeries.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return ucapi.EVChargeStrategyTypeUnknown + } + + // without time series slots, there is no known strategy + if data[0].TimeSeriesSlot == nil || len(data[0].TimeSeriesSlot) == 0 { + return ucapi.EVChargeStrategyTypeUnknown + } + + // get the value for the first slot + firstSlot := data[0].TimeSeriesSlot[0] + + switch { + case firstSlot.Duration == nil: + // if value is > 0 and duration does not exist, the EV is direct charging + if firstSlot.Value != nil && firstSlot.Value.GetValue() > 0 { + return ucapi.EVChargeStrategyTypeDirectCharging + } + + // maxValue will show the maximum amount the battery could take + return ucapi.EVChargeStrategyTypeNoDemand + + case firstSlot.Duration != nil: + if _, err := firstSlot.Duration.GetTimeDuration(); err != nil { + // we got an invalid duration + return ucapi.EVChargeStrategyTypeUnknown + } + + if firstSlot.MinValue != nil && firstSlot.MinValue.GetValue() > 0 { + return ucapi.EVChargeStrategyTypeMinSoC + } + + if firstSlot.Value != nil { + if firstSlot.Value.GetValue() > 0 { + // there is demand and a duration + return ucapi.EVChargeStrategyTypeTimedCharging + } + + return ucapi.EVChargeStrategyTypeNoDemand + } + } + + return ucapi.EVChargeStrategyTypeUnknown +} + +// returns the current energy demand in Wh and the duration +func (e *CemCEVC) EnergyDemand(entity spineapi.EntityRemoteInterface) (ucapi.Demand, error) { + demand := ucapi.Demand{} + + if !e.IsCompatibleEntity(entity) { + return demand, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return demand, api.ErrDataNotAvailable + } + + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + } + data, err := evTimeSeries.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return demand, api.ErrDataNotAvailable + } + + // we need at least a time series slot + if data[0].TimeSeriesSlot == nil { + return demand, api.ErrDataNotAvailable + } + + // get the value for the first slot, ignore all others, which + // in the tests so far always have min/max/value 0 + firstSlot := data[0].TimeSeriesSlot[0] + if firstSlot.MinValue != nil { + demand.MinDemand = firstSlot.MinValue.GetValue() + } + if firstSlot.Value != nil { + demand.OptDemand = firstSlot.Value.GetValue() + } + if firstSlot.MaxValue != nil { + demand.MaxDemand = firstSlot.MaxValue.GetValue() + } + if firstSlot.Duration != nil { + if tempDuration, err := firstSlot.Duration.GetTimeDuration(); err == nil { + demand.DurationUntilEnd = tempDuration.Seconds() + } + } + + // start time has to be defined either in TimePeriod or the first slot + relStartTime := time.Duration(0) + + startTimeSet := false + if data[0].TimePeriod != nil && data[0].TimePeriod.StartTime != nil { + if temp, err := data[0].TimePeriod.StartTime.GetTimeDuration(); err == nil { + relStartTime = temp + startTimeSet = true + } + } + + if !startTimeSet { + if firstSlot.TimePeriod != nil && firstSlot.TimePeriod.StartTime != nil { + if temp, err := firstSlot.TimePeriod.StartTime.GetTimeDuration(); err == nil { + relStartTime = temp + } + } + } + + demand.DurationUntilStart = relStartTime.Seconds() + + return demand, nil +} diff --git a/usecases/cem/cevc/public_scen1_test.go b/usecases/cem/cevc/public_scen1_test.go new file mode 100644 index 00000000..2de28d25 --- /dev/null +++ b/usecases/cem/cevc/public_scen1_test.go @@ -0,0 +1,295 @@ +package cevc + +import ( + "time" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_ChargeStrategy() { + data := s.sut.ChargeStrategy(s.mockRemoteEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeUnknown, data) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeUnknown, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeUnknown, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: util.Ptr(model.DeviceConfigurationKeyValueStringType(model.DeviceConfigurationKeyValueStringTypeISO151182ED2)), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeUnknown, data) + + timeDescData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDescData, nil, nil) + assert.Nil(s.T(), fErr) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeUnknown, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeNoDemand, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT0S")), + Value: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeNoDemand, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeDirectCharging, data) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + Duration: model.NewDurationType(2 * time.Hour), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.ChargeStrategy(s.evEntity) + assert.Equal(s.T(), ucapi.EVChargeStrategyTypeTimedCharging, data) +} + +func (s *CEVCSuite) Test_EnergySingleDemand() { + demand, err := s.sut.EnergyDemand(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeDescData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDescData, nil, nil) + assert.Nil(s.T(), fErr) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + MinValue: model.NewScaledNumberType(1000), + Value: model.NewScaledNumberType(10000), + MaxValue: model.NewScaledNumberType(100000), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1000.0, demand.MinDemand) + assert.Equal(s.T(), 10000.0, demand.OptDemand) + assert.Equal(s.T(), 100000.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Value: model.NewScaledNumberType(10000), + Duration: model.NewDurationType(2 * time.Hour), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 10000.0, demand.OptDemand) + assert.Equal(s.T(), 0.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), time.Duration(2*time.Hour).Seconds(), demand.DurationUntilEnd) +} diff --git a/usecases/cem/cevc/public_scen2.go b/usecases/cem/cevc/public_scen2.go new file mode 100644 index 00000000..e1a3d5a2 --- /dev/null +++ b/usecases/cem/cevc/public_scen2.go @@ -0,0 +1,181 @@ +package cevc + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// returns the constraints for the time slots +func (e *CemCEVC) TimeSlotConstraints(entity spineapi.EntityRemoteInterface) (ucapi.TimeSlotConstraints, error) { + result := ucapi.TimeSlotConstraints{} + + if !e.IsCompatibleEntity(entity) { + return result, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return result, api.ErrDataNotAvailable + } + + constraints, err := evTimeSeries.GetConstraints() + if err != nil { + return result, err + } + + // only use the first constraint + constraint := constraints[0] + + if constraint.SlotCountMin != nil { + result.MinSlots = uint(*constraint.SlotCountMin) + } + if constraint.SlotCountMax != nil { + result.MaxSlots = uint(*constraint.SlotCountMax) + } + if constraint.SlotDurationMin != nil { + if duration, err := constraint.SlotDurationMin.GetTimeDuration(); err == nil { + result.MinSlotDuration = duration + } + } + if constraint.SlotDurationMax != nil { + if duration, err := constraint.SlotDurationMax.GetTimeDuration(); err == nil { + result.MaxSlotDuration = duration + } + } + if constraint.SlotDurationStepSize != nil { + if duration, err := constraint.SlotDurationStepSize.GetTimeDuration(); err == nil { + result.SlotDurationStepSize = duration + } + } + + return result, nil +} + +// send power limits to the EV +// if no data is provided, default power limits with the max possible value for 7 days will be sent +func (e *CemCEVC) WritePowerLimits(entity spineapi.EntityRemoteInterface, data []ucapi.DurationSlotValue) error { + if !e.IsCompatibleEntity(entity) { + return api.ErrNoCompatibleEntity + } + + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return api.ErrDataNotAvailable + } + + if len(data) == 0 { + data, err = e.defaultPowerLimits(entity) + if err != nil { + return err + } + } + + constraints, err := e.TimeSlotConstraints(entity) + if err != nil { + return err + } + + if constraints.MinSlots != 0 && constraints.MinSlots > uint(len(data)) { + return errors.New("too few charge slots provided") + } + + if constraints.MaxSlots != 0 && constraints.MaxSlots < uint(len(data)) { + return errors.New("too many charge slots provided") + } + + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + } + desc, err := evTimeSeries.GetDescriptionsForFilter(filter) + if err != nil || len(desc) == 0 { + return api.ErrDataNotAvailable + } + + timeSeriesSlots := []model.TimeSeriesSlotType{} + var totalDuration time.Duration + for index, slot := range data { + relativeStart := totalDuration + + timeSeriesSlot := model.TimeSeriesSlotType{ + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(index)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(relativeStart), + }, + MaxValue: model.NewScaledNumberType(slot.Value), + } + + // the last slot also needs an End Time + if index == len(data)-1 { + relativeEndTime := relativeStart + slot.Duration + timeSeriesSlot.TimePeriod.EndTime = model.NewAbsoluteOrRelativeTimeTypeFromDuration(relativeEndTime) + } + timeSeriesSlots = append(timeSeriesSlots, timeSeriesSlot) + + totalDuration += slot.Duration + } + + timeSeriesData := model.TimeSeriesDataType{ + TimeSeriesId: desc[0].TimeSeriesId, + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(totalDuration), + }, + TimeSeriesSlot: timeSeriesSlots, + } + + _, err = evTimeSeries.WriteData([]model.TimeSeriesDataType{timeSeriesData}) + + return err +} + +func (e *CemCEVC) defaultPowerLimits(entity spineapi.EntityRemoteInterface) ([]ucapi.DurationSlotValue, error) { + // send default power limits for the maximum timeframe + // to fullfill spec, as there is no data provided + logging.Log().Info("Fallback sending default power limits") + + evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + logging.Log().Error("electrical connection feature not found") + return nil, err + } + + filter := model.ElectricalConnectionParameterDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + } + paramDesc, err := evElectricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(paramDesc) == 0 || paramDesc[0].ParameterId == nil { + logging.Log().Error("Error getting parameter descriptions:", err) + return nil, err + } + + filter2 := model.ElectricalConnectionPermittedValueSetDataType{ + ParameterId: paramDesc[0].ParameterId, + } + permitted, err := evElectricalConnection.GetPermittedValueSetForFilter(filter2) + if err != nil || len(permitted) == 0 { + logging.Log().Error("Error getting permitted values:", err) + return nil, err + } + + if len(permitted[0].PermittedValueSet) == 0 || len(permitted[0].PermittedValueSet[0].Range) == 0 { + text := "No permitted value set available" + logging.Log().Error(text) + return nil, errors.New(text) + } + + data := []ucapi.DurationSlotValue{ + { + Duration: 7 * time.Hour * 24, + Value: permitted[0].PermittedValueSet[0].Range[0].Max.GetValue(), + }, + } + return data, nil +} diff --git a/usecases/cem/cevc/public_scen2_test.go b/usecases/cem/cevc/public_scen2_test.go new file mode 100644 index 00000000..b77d539c --- /dev/null +++ b/usecases/cem/cevc/public_scen2_test.go @@ -0,0 +1,220 @@ +package cevc + +import ( + "testing" + "time" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_TimeSlotConstraints() { + constraints, err := s.sut.TimeSlotConstraints(s.mockRemoteEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.Equal(s.T(), time.Duration(0), constraints.MinSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.SlotDurationStepSize) + assert.NotEqual(s.T(), err, nil) + + constraints, err = s.sut.TimeSlotConstraints(s.evEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.Equal(s.T(), time.Duration(0), constraints.MinSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.MaxSlotDuration) + assert.Equal(s.T(), time.Duration(0), constraints.SlotDurationStepSize) + assert.NotEqual(s.T(), err, nil) + + constData := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: util.Ptr(model.TimeSeriesSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(10)), + SlotDurationMin: model.NewDurationType(1 * time.Minute), + SlotDurationMax: model.NewDurationType(60 * time.Minute), + SlotDurationStepSize: model.NewDurationType(1 * time.Minute), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + constraints, err = s.sut.TimeSlotConstraints(s.evEntity) + assert.Equal(s.T(), uint(1), constraints.MinSlots) + assert.Equal(s.T(), uint(10), constraints.MaxSlots) + assert.Equal(s.T(), time.Duration(1*time.Minute), constraints.MinSlotDuration) + assert.Equal(s.T(), time.Duration(1*time.Hour), constraints.MaxSlotDuration) + assert.Equal(s.T(), time.Duration(1*time.Minute), constraints.SlotDurationStepSize) + assert.Equal(s.T(), err, nil) +} + +func (s *CEVCSuite) Test_WritePowerLimits() { + data := []ucapi.DurationSlotValue{} + + err := s.sut.WritePowerLimits(s.mockRemoteEntity, data) + assert.NotNil(s.T(), err) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + elParamDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamDesc, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + elPermDesc := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, elPermDesc, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + elPermDesc = &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Range: []model.ScaledNumberRangeType{ + { + Max: model.NewScaledNumberType(16), + }, + }, + }, + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, elPermDesc, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + descData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data) + assert.NotNil(s.T(), err) + + type dataStruct struct { + error bool + minSlots, maxSlots uint + slots []ucapi.DurationSlotValue + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "too few slots", + []dataStruct{ + { + true, 2, 2, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, { + "too many slots", + []dataStruct{ + { + true, 1, 1, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, + { + "1 slot", + []dataStruct{ + { + false, 1, 1, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + }, + }, + }, + }, + { + "2 slots", + []dataStruct{ + { + false, 1, 2, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 11000}, + {Duration: 30 * time.Minute, Value: 5000}, + }, + }, + }, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + for _, data := range tc.data { + constData := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + SlotCountMin: util.Ptr(model.TimeSeriesSlotCountType(data.minSlots)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(data.maxSlots)), + }, + }, + } + + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WritePowerLimits(s.evEntity, data.slots) + if data.error { + assert.NotNil(t, err) + continue + } + + assert.Nil(t, err) + } + }) + } +} diff --git a/usecases/cem/cevc/public_scen3.go b/usecases/cem/cevc/public_scen3.go new file mode 100644 index 00000000..e45a79b2 --- /dev/null +++ b/usecases/cem/cevc/public_scen3.go @@ -0,0 +1,279 @@ +package cevc + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// returns the minimum and maximum number of incentive slots allowed +func (e *CemCEVC) IncentiveConstraints(entity spineapi.EntityRemoteInterface) (ucapi.IncentiveSlotConstraints, error) { + result := ucapi.IncentiveSlotConstraints{} + + if !e.IsCompatibleEntity(entity) { + return result, api.ErrNoCompatibleEntity + } + + evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, entity) + if err != nil { + return result, api.ErrDataNotAvailable + } + + constraints, err := evIncentiveTable.GetConstraints() + if err != nil { + return result, err + } + + // only use the first constraint + constraint := constraints[0] + + if constraint.IncentiveSlotConstraints.SlotCountMin != nil { + result.MinSlots = uint(*constraint.IncentiveSlotConstraints.SlotCountMin) + } + if constraint.IncentiveSlotConstraints.SlotCountMax != nil { + result.MaxSlots = uint(*constraint.IncentiveSlotConstraints.SlotCountMax) + } + + return result, nil +} + +// inform the EVSE about used currency and boundary units +// +// SPINE UC CoordinatedEVCharging 2.4.3 +func (e *CemCEVC) WriteIncentiveTableDescriptions(entity spineapi.EntityRemoteInterface, data []ucapi.IncentiveTariffDescription) error { + if !e.IsCompatibleEntity(entity) { + return api.ErrNoCompatibleEntity + } + + evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, entity) + if err != nil { + logging.Log().Error("incentivetable feature not found") + return err + } + + filter := model.TariffDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + } + descriptions, err := evIncentiveTable.GetDescriptionsForFilter(filter) + if err != nil { + logging.Log().Error(err) + return err + } + + // default tariff + // + // - tariff, min 1 + // each tariff has + // - tiers: min 1, max 3 + // each tier has: + // - boundaries: min 1, used for different power limits, e.g. 0-1kW x€, 1-3kW y€, ... + // - incentives: min 1, max 3 + // - price/costs (absolute or relative) + // - renewable energy percentage + // - CO2 emissions + // + // limit this to + // - 1 tariff + // - 1 tier + // - 1 boundary + // - 1 incentive (price) + // incentive type has to be the same for all sent power limits! + descData := []model.IncentiveTableDescriptionType{ + { + TariffDescription: descriptions[0].TariffDescription, + Tier: []model.IncentiveTableDescriptionTierType{ + { + TierDescription: &model.TierDescriptionDataType{ + TierId: util.Ptr(model.TierIdType(0)), + TierType: util.Ptr(model.TierTypeTypeDynamicCost), + }, + BoundaryDescription: []model.TierBoundaryDescriptionDataType{ + { + BoundaryId: util.Ptr(model.TierBoundaryIdType(0)), + BoundaryType: util.Ptr(model.TierBoundaryTypeTypePowerBoundary), + BoundaryUnit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + }, + IncentiveDescription: []model.IncentiveDescriptionDataType{ + { + IncentiveId: util.Ptr(model.IncentiveIdType(0)), + IncentiveType: util.Ptr(model.IncentiveTypeTypeAbsoluteCost), + Currency: util.Ptr(model.CurrencyTypeEur), + }, + }, + }, + }, + }, + } + + if len(data) > 0 && len(data[0].Tiers) > 0 { + newDescData := []model.IncentiveTableDescriptionType{} + allDataPresent := false + + for index, tariff := range data { + tariffDesc := descriptions[0].TariffDescription + if len(descriptions) > index { + tariffDesc = descriptions[index].TariffDescription + } + + newTariff := model.IncentiveTableDescriptionType{ + TariffDescription: tariffDesc, + } + + tierData := []model.IncentiveTableDescriptionTierType{} + for _, tier := range tariff.Tiers { + newTier := model.IncentiveTableDescriptionTierType{} + + newTier.TierDescription = &model.TierDescriptionDataType{ + TierId: util.Ptr(model.TierIdType(tier.Id)), + TierType: util.Ptr(tier.Type), + } + + boundaryDescription := []model.TierBoundaryDescriptionDataType{} + for _, boundary := range tier.Boundaries { + newBoundary := model.TierBoundaryDescriptionDataType{ + BoundaryId: util.Ptr(model.TierBoundaryIdType(boundary.Id)), + BoundaryType: util.Ptr(boundary.Type), + BoundaryUnit: util.Ptr(boundary.Unit), + } + boundaryDescription = append(boundaryDescription, newBoundary) + } + newTier.BoundaryDescription = boundaryDescription + + incentiveDescription := []model.IncentiveDescriptionDataType{} + for _, incentive := range tier.Incentives { + newIncentive := model.IncentiveDescriptionDataType{ + IncentiveId: util.Ptr(model.IncentiveIdType(incentive.Id)), + IncentiveType: util.Ptr(incentive.Type), + } + if incentive.Currency != "" { + newIncentive.Currency = util.Ptr(incentive.Currency) + } + incentiveDescription = append(incentiveDescription, newIncentive) + } + newTier.IncentiveDescription = incentiveDescription + + if len(newTier.BoundaryDescription) > 0 && + len(newTier.IncentiveDescription) > 0 { + allDataPresent = true + } + tierData = append(tierData, newTier) + } + + newTariff.Tier = tierData + + newDescData = append(newDescData, newTariff) + } + + if allDataPresent { + descData = newDescData + } + } + + _, err = evIncentiveTable.WriteDescriptions(descData) + if err != nil { + logging.Log().Error(err) + return err + } + + return nil +} + +// send incentives to the EV +// if no data is provided, default incentives with the same price for 7 days will be sent +func (e *CemCEVC) WriteIncentives(entity spineapi.EntityRemoteInterface, data []ucapi.DurationSlotValue) error { + if !e.IsCompatibleEntity(entity) { + return api.ErrNoCompatibleEntity + } + + evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, entity) + if err != nil { + return api.ErrDataNotAvailable + } + + if len(data) == 0 { + // send default incentives for the maximum timeframe + // to fullfill spec, as there is no data provided + logging.Log().Info("Fallback sending default incentives") + data = []ucapi.DurationSlotValue{ + {Duration: 7 * time.Hour * 24, Value: 0.30}, + } + } + + constraints, err := e.IncentiveConstraints(entity) + if err != nil { + return err + } + + if constraints.MinSlots != 0 && constraints.MinSlots > uint(len(data)) { + return errors.New("too few charge slots provided") + } + + if constraints.MaxSlots != 0 && constraints.MaxSlots < uint(len(data)) { + return errors.New("too many charge slots provided") + } + + incentiveSlots := []model.IncentiveTableIncentiveSlotType{} + var totalDuration time.Duration + for index, slot := range data { + relativeStart := totalDuration + + timeInterval := &model.TimeTableDataType{ + StartTime: &model.AbsoluteOrRecurringTimeType{ + Relative: model.NewDurationType(relativeStart), + }, + } + + // the last slot also needs an End Time + if index == len(data)-1 { + relativeEndTime := relativeStart + slot.Duration + timeInterval.EndTime = &model.AbsoluteOrRecurringTimeType{ + Relative: model.NewDurationType(relativeEndTime), + } + } + + incentiveSlot := model.IncentiveTableIncentiveSlotType{ + TimeInterval: timeInterval, + Tier: []model.IncentiveTableTierType{ + { + Tier: &model.TierDataType{ + TierId: util.Ptr(model.TierIdType(0)), + }, + Boundary: []model.TierBoundaryDataType{ + { + BoundaryId: util.Ptr(model.TierBoundaryIdType(0)), // only 1 boundary exists + LowerBoundaryValue: model.NewScaledNumberType(0), + }, + }, + Incentive: []model.IncentiveDataType{ + { + IncentiveId: util.Ptr(model.IncentiveIdType(0)), // always use price + Value: model.NewScaledNumberType(slot.Value), + }, + }, + }, + }, + } + incentiveSlots = append(incentiveSlots, incentiveSlot) + + totalDuration += slot.Duration + } + + incentiveData := model.IncentiveTableType{ + Tariff: &model.TariffDataType{ + TariffId: util.Ptr(model.TariffIdType(0)), + }, + IncentiveSlot: incentiveSlots, + } + + _, err = evIncentiveTable.WriteValues([]model.IncentiveTableType{incentiveData}) + + return err +} diff --git a/usecases/cem/cevc/public_scen3_test.go b/usecases/cem/cevc/public_scen3_test.go new file mode 100644 index 00000000..c509c74e --- /dev/null +++ b/usecases/cem/cevc/public_scen3_test.go @@ -0,0 +1,230 @@ +package cevc + +import ( + "testing" + "time" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_IncentiveConstraints() { + constraints, err := s.sut.IncentiveConstraints(s.mockRemoteEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.NotEqual(s.T(), err, nil) + + constraints, err = s.sut.IncentiveConstraints(s.evEntity) + assert.Equal(s.T(), uint(0), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.NotEqual(s.T(), err, nil) + + constData := &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + constraints, err = s.sut.IncentiveConstraints(s.evEntity) + assert.Equal(s.T(), uint(1), constraints.MinSlots) + assert.Equal(s.T(), uint(10), constraints.MaxSlots) + assert.Equal(s.T(), err, nil) + + constData = &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + constraints, err = s.sut.IncentiveConstraints(s.evEntity) + assert.Equal(s.T(), uint(1), constraints.MinSlots) + assert.Equal(s.T(), uint(0), constraints.MaxSlots) + assert.Equal(s.T(), err, nil) +} + +func (s *CEVCSuite) Test_WriteIncentiveTableDescriptions() { + data := []ucapi.IncentiveTariffDescription{} + + err := s.sut.WriteIncentiveTableDescriptions(s.mockRemoteEntity, data) + assert.NotNil(s.T(), err) + + err = s.sut.WriteIncentiveTableDescriptions(s.evEntity, data) + assert.NotNil(s.T(), err) + + descData := &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableDescriptionData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WriteIncentiveTableDescriptions(s.evEntity, data) + assert.Nil(s.T(), err) + + data = []ucapi.IncentiveTariffDescription{ + { + Tiers: []ucapi.IncentiveTableDescriptionTier{ + { + Id: 0, + Type: model.TierTypeTypeDynamicCost, + Boundaries: []ucapi.TierBoundaryDescription{ + { + Id: 0, + Type: model.TierBoundaryTypeTypePowerBoundary, + Unit: model.UnitOfMeasurementTypeW, + }, + }, + Incentives: []ucapi.IncentiveDescription{ + { + Id: 0, + Type: model.IncentiveTypeTypeAbsoluteCost, + Currency: model.CurrencyTypeEur, + }, + }, + }, + }, + }, + } + + err = s.sut.WriteIncentiveTableDescriptions(s.evEntity, data) + assert.Nil(s.T(), err) +} + +func (s *CEVCSuite) Test_WriteIncentives() { + data := []ucapi.DurationSlotValue{} + + err := s.sut.WriteIncentives(s.mockRemoteEntity, data) + assert.NotNil(s.T(), err) + + err = s.sut.WriteIncentives(s.evEntity, data) + assert.NotNil(s.T(), err) + + constData := &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(1)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(10)), + }, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WriteIncentives(s.evEntity, data) + assert.Nil(s.T(), err) + + type dataStruct struct { + error bool + minSlots, maxSlots uint + slots []ucapi.DurationSlotValue + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "too few slots", + []dataStruct{ + { + true, 2, 2, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, { + "too many slots", + []dataStruct{ + { + true, 1, 1, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, + { + "1 slot", + []dataStruct{ + { + false, 1, 1, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + }, + }, + }, + }, + { + "2 slots", + []dataStruct{ + { + false, 1, 2, + []ucapi.DurationSlotValue{ + {Duration: time.Hour, Value: 0.1}, + {Duration: 30 * time.Minute, Value: 0.2}, + }, + }, + }, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + for _, data := range tc.data { + constData = &model.IncentiveTableConstraintsDataType{ + IncentiveTableConstraints: []model.IncentiveTableConstraintsType{ + { + IncentiveSlotConstraints: &model.TimeTableConstraintsDataType{ + SlotCountMin: util.Ptr(model.TimeSlotCountType(data.minSlots)), + SlotCountMax: util.Ptr(model.TimeSlotCountType(data.maxSlots)), + }, + }, + }, + } + + fErr := rFeature.UpdateData(model.FunctionTypeIncentiveTableConstraintsData, constData, nil, nil) + assert.Nil(s.T(), fErr) + + err = s.sut.WriteIncentives(s.evEntity, data.slots) + if data.error { + assert.NotNil(t, err) + continue + } + + assert.Nil(t, err) + } + }) + } +} diff --git a/usecases/cem/cevc/public_scen4.go b/usecases/cem/cevc/public_scen4.go new file mode 100644 index 00000000..717c81e6 --- /dev/null +++ b/usecases/cem/cevc/public_scen4.go @@ -0,0 +1,153 @@ +package cevc + +import ( + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +func (e *CemCEVC) ChargePlanConstraints(entity spineapi.EntityRemoteInterface) ([]ucapi.DurationSlotValue, error) { + constraints := []ucapi.DurationSlotValue{} + + if !e.IsCompatibleEntity(entity) { + return constraints, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return constraints, api.ErrDataNotAvailable + } + + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + } + data, err := evTimeSeries.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return constraints, api.ErrDataNotAvailable + } + + // we need at least a time series slot + if data[0].TimeSeriesSlot == nil { + return constraints, api.ErrDataNotAvailable + } + + // get the values for all slots + for _, slot := range data[0].TimeSeriesSlot { + newSlot := ucapi.DurationSlotValue{} + + if slot.Duration != nil { + if duration, err := slot.Duration.GetTimeDuration(); err == nil { + newSlot.Duration = duration + } + } else if slot.TimePeriod != nil { + var slotStart, slotEnd time.Time + if slot.TimePeriod.StartTime != nil { + if time, err := slot.TimePeriod.StartTime.GetTime(); err == nil { + slotStart = time + } + } + if slot.TimePeriod.EndTime != nil { + if time, err := slot.TimePeriod.EndTime.GetTime(); err == nil { + slotEnd = time + } + } + newSlot.Duration = slotEnd.Sub(slotStart) + } + + if slot.MaxValue != nil { + newSlot.Value = slot.MaxValue.GetValue() + } + + constraints = append(constraints, newSlot) + } + + return constraints, nil +} + +func (e *CemCEVC) ChargePlan(entity spineapi.EntityRemoteInterface) (ucapi.ChargePlan, error) { + plan := ucapi.ChargePlan{} + + if !e.IsCompatibleEntity(entity) { + return plan, api.ErrNoCompatibleEntity + } + + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return plan, api.ErrDataNotAvailable + } + + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + } + data, err := evTimeSeries.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return plan, api.ErrDataNotAvailable + } + + // we need at least a time series slot + if data[0].TimeSeriesSlot == nil { + return plan, api.ErrDataNotAvailable + } + + startAvailable := false + // check the start time relative to now of the plan, default is now + currentStart := time.Now() + currentEnd := currentStart + if data[0].TimePeriod != nil && data[0].TimePeriod.StartTime != nil { + if start, err := data[0].TimePeriod.StartTime.GetTimeDuration(); err == nil { + currentStart = currentStart.Add(start) + startAvailable = true + } + } + + // get the values for all slots + for index, slot := range data[0].TimeSeriesSlot { + newSlot := ucapi.ChargePlanSlotValue{} + + slotStartDefined := false + if index == 0 && startAvailable && (slot.TimePeriod == nil || slot.TimePeriod.StartTime == nil) { + newSlot.Start = currentStart + slotStartDefined = true + } + if slot.TimePeriod != nil && slot.TimePeriod.StartTime != nil { + if time, err := slot.TimePeriod.StartTime.GetTime(); err == nil { + newSlot.Start = time + slotStartDefined = true + } + } + if !slotStartDefined { + newSlot.Start = currentEnd + } + + if slot.Duration != nil { + if duration, err := slot.Duration.GetTimeDuration(); err == nil { + newSlot.End = newSlot.Start.Add(duration) + currentEnd = newSlot.End + } + } else if slot.TimePeriod != nil && slot.TimePeriod.EndTime != nil { + if time, err := slot.TimePeriod.StartTime.GetTime(); err == nil { + newSlot.End = time + currentEnd = newSlot.End + } + } + + if slot.Value != nil { + newSlot.Value = slot.Value.GetValue() + } + if slot.MinValue != nil { + newSlot.MinValue = slot.MinValue.GetValue() + } + if slot.MaxValue != nil { + newSlot.MaxValue = slot.MaxValue.GetValue() + } + + plan.Slots = append(plan.Slots, newSlot) + } + + return plan, nil +} diff --git a/usecases/cem/cevc/public_scen4_test.go b/usecases/cem/cevc/public_scen4_test.go new file mode 100644 index 00000000..514f6436 --- /dev/null +++ b/usecases/cem/cevc/public_scen4_test.go @@ -0,0 +1,174 @@ +package cevc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_ChargePlanConstaints() { + _, err := s.sut.ChargePlanConstraints(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.NotNil(s.T(), err) + + descData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.NotNil(s.T(), err) + + data := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.NotNil(s.T(), err) + + data = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT30S"), + EndTime: model.NewAbsoluteOrRelativeTimeType("PT1M"), + }, + MaxValue: model.NewScaledNumberType(4201), + }, + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesListData, data, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlanConstraints(s.evEntity) + assert.Nil(s.T(), err) +} + +func (s *CEVCSuite) Test_ChargePlan() { + _, err := s.sut.ChargePlan(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ChargePlan(s.evEntity) + assert.NotNil(s.T(), err) + + descData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlan(s.evEntity) + assert.NotNil(s.T(), err) + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimePeriod: &model.TimePeriodType{}, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT30S"), + EndTime: model.NewAbsoluteOrRelativeTimeType("PT1M"), + }, + Value: model.NewScaledNumberType(5), + MinValue: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(10), + }, + }, + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT5M36S")), + MaxValue: model.NewScaledNumberType(4201), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT30S"), + EndTime: model.NewAbsoluteOrRelativeTimeType("PT1M"), + }, + Value: model.NewScaledNumberType(5), + MinValue: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(10), + }, + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.ChargePlan(s.evEntity) + assert.Nil(s.T(), err) +} diff --git a/usecases/cem/cevc/public_test.go b/usecases/cem/cevc/public_test.go new file mode 100644 index 00000000..ddb3bb5b --- /dev/null +++ b/usecases/cem/cevc/public_test.go @@ -0,0 +1,339 @@ +package cevc + +import ( + "time" + + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_CoordinatedChargingScenarios() { + timeConst := &model.TimeSeriesConstraintsListDataType{ + TimeSeriesConstraintsData: []model.TimeSeriesConstraintsDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + SlotCountMax: util.Ptr(model.TimeSeriesSlotCountType(30)), + }, + }, + } + + rTimeFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr := rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesConstraintsListData, timeConst, nil, nil) + assert.Nil(s.T(), fErr) + + timeDesc := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeSingleDemand), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDesc, nil, nil) + assert.Nil(s.T(), fErr) + + incDesc := &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(1)), + TariffWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(false), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + } + + rIncentiveFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr = rIncentiveFeature.UpdateData(model.FunctionTypeIncentiveTableDescriptionData, incDesc, nil, nil) + assert.Nil(s.T(), fErr) + + // demand, No Profile No Timer demand + + timeData := &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Value: model.NewScaledNumberType(0), + MaxValue: model.NewScaledNumberType(74690), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err := s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 0.0, demand.OptDemand) + assert.Equal(s.T(), 74690.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + // the final plan + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT18H3M7S")), + MaxValue: model.NewScaledNumberType(4163), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("PT42M")), + MaxValue: model.NewScaledNumberType(2736), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + // demand, profile + timer with 80% target and no climate, minSoC reached + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P2DT4H40M36S")), + Value: model.NewScaledNumberType(53400), + MaxValue: model.NewScaledNumberType(74690), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0.0, demand.MinDemand) + assert.Equal(s.T(), 53400.0, demand.OptDemand) + assert.Equal(s.T(), 74690.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), time.Duration(time.Hour*52+time.Minute*40+time.Second*36).Seconds(), demand.DurationUntilEnd) + + // the final plan + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("P1DT15H24M24S")), + MaxValue: model.NewScaledNumberType(0), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("PT12H35M50S")), + MaxValue: model.NewScaledNumberType(4163), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(2)), + Duration: util.Ptr(model.DurationType("PT40M22S")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + // demand, profile with 25% min SoC, minSoC not reached, no timer + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT8M42S")), + MaxValue: model.NewScaledNumberType(4212), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Value: model.NewScaledNumberType(600), + MinValue: model.NewScaledNumberType(600), + MaxValue: model.NewScaledNumberType(75600), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) + + demand, err = s.sut.EnergyDemand(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 600.0, demand.MinDemand) + assert.Equal(s.T(), 600.0, demand.OptDemand) + assert.Equal(s.T(), 75600.0, demand.MaxDemand) + assert.Equal(s.T(), 0.0, demand.DurationUntilStart) + assert.Equal(s.T(), 0.0, demand.DurationUntilEnd) + + // the final plan + + timeData = &model.TimeSeriesListDataType{ + TimeSeriesData: []model.TimeSeriesDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimePeriod: &model.TimePeriodType{ + StartTime: model.NewAbsoluteOrRelativeTimeType("PT0S"), + }, + TimeSeriesSlot: []model.TimeSeriesSlotType{ + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(0)), + Duration: util.Ptr(model.DurationType("PT8M42S")), + MaxValue: model.NewScaledNumberType(4212), + }, + { + TimeSeriesSlotId: util.Ptr(model.TimeSeriesSlotIdType(1)), + Duration: util.Ptr(model.DurationType("P1D")), + MaxValue: model.NewScaledNumberType(0), + }, + }, + }, + }, + } + + fErr = rTimeFeature.UpdateData(model.FunctionTypeTimeSeriesListData, timeData, nil, nil) + assert.Nil(s.T(), fErr) +} + +/* +func requestIncentiveUpdate(t *testing.T, datagram model.DatagramType, localDevice api.DeviceLocal, remoteDevice api.DeviceRemote) { + cmd := []model.CmdType{{ + IncentiveTableDescriptionData: &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(1)), + TariffWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(true), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) +} + +func requestPowerTableUpdate(t *testing.T, datagram model.DatagramType, localDevice api.DeviceLocal, remoteDevice api.DeviceRemote) { + cmd := []model.CmdType{{ + TimeSeriesDescriptionListData: &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(1)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(true), + UpdateRequired: util.Ptr(true), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(2)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypePlan), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(3)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + TimeSeriesWriteable: util.Ptr(false), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + }, + }, + }, + }} + + datagram.Payload.Cmd = cmd + + err := localDevice.ProcessCmd(datagram, remoteDevice) + assert.Nil(t, err) +} +*/ diff --git a/usecases/cem/cevc/testhelper_test.go b/usecases/cem/cevc/testhelper_test.go new file mode 100644 index 00000000..01ebe11b --- /dev/null +++ b/usecases/cem/cevc/testhelper_test.go @@ -0,0 +1,199 @@ +package cevc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestCEVCSuite(t *testing.T) { + suite.Run(t, new(CEVCSuite)) +} + +type CEVCSuite struct { + suite.Suite + + sut *CemCEVC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *CEVCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *CEVCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + ops := map[model.FunctionType]spineapi.OperationsInterface{} + mockRemoteFeature.EXPECT().Operations().Return(ops).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemCEVC(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeTimeSeries, + []model.FunctionType{ + model.FunctionTypeTimeSeriesConstraintsListData, + model.FunctionTypeTimeSeriesDescriptionListData, + model.FunctionTypeTimeSeriesListData, + }, + }, + {model.FeatureTypeTypeIncentiveTable, + []model.FunctionType{ + model.FunctionTypeIncentiveTableConstraintsData, + model.FunctionTypeIncentiveTableDescriptionData, + model.FunctionTypeIncentiveTableData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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 +} diff --git a/usecases/cem/cevc/types.go b/usecases/cem/cevc/types.go new file mode 100644 index 00000000..b2df5399 --- /dev/null +++ b/usecases/cem/cevc/types.go @@ -0,0 +1,46 @@ +package cevc + +import "github.com/enbility/eebus-go/api" + +const ( + // Scenario 1 + + // EV provided an energy demand + // + // Use `EnergyDemand` to get the current data + DataUpdateEnergyDemand api.EventType = "cem-cevc-DataUpdateEnergyDemand" + + // Scenario 2 + + // EV provided a charge plan constraints + // + // Use `TimeSlotConstraints` to get the current data + DataUpdateTimeSlotConstraints api.EventType = "cem-cevc-DataUpdateTimeSlotConstraints" + + // Scenario 3 + + // EV incentive table data updated + // + // Use `IncentiveConstraints` to get the current data + DataUpdateIncentiveTable api.EventType = "cem-cevc-DataUpdateIncentiveTable" + + // EV requested an incentive table, call to WriteIncentiveTableDescriptions required + DataRequestedIncentiveTableDescription api.EventType = "cem-cevc-DataRequestedIncentiveTableDescription" + + // Scenario 2 & 3 + + // EV requested power limits, call to WritePowerLimits and WriteIncentives required + DataRequestedPowerLimitsAndIncentives api.EventType = "cem-cevc-DataRequestedPowerLimitsAndIncentives" + + // Scenario 4 + + // EV provided a charge plan + // + // Use `ChargePlanConstraints` to get the current data + DataUpdateChargePlanConstraints api.EventType = "cem-cevc-DataUpdateChargePlanConstraints" + + // EV provided a charge plan + // + // Use `ChargePlan` to get the current data + DataUpdateChargePlan api.EventType = "cem-cevc-DataUpdateChargePlan" +) diff --git a/usecases/cem/cevc/usecase.go b/usecases/cem/cevc/usecase.go new file mode 100644 index 00000000..461fa6f3 --- /dev/null +++ b/usecases/cem/cevc/usecase.go @@ -0,0 +1,109 @@ +package cevc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CemCEVC struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemCEVCInterface = (*CemCEVC)(nil) + +func NewCemCEVC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemCEVC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeCoordinatedEVCharging, + "1.0.1", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3}, + eventCB, + validEntityTypes, + ) + + uc := &CemCEVC{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemCEVC) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeTimeSeries, + model.FeatureTypeTypeIncentiveTable, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemCEVC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeEV, + e.UseCaseName, + []model.UseCaseScenarioSupportType{2, 3, 4, 5, 6, 7, 8}, + []model.FeatureTypeType{ + model.FeatureTypeTypeTimeSeries, + model.FeatureTypeTypeIncentiveTable, + }, + ) { + return false, nil + } + + // check for required features + evTimeSeries, err := client.NewTimeSeries(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + evIncentiveTable, err := client.NewIncentiveTable(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if timeseries descriptions contains constraints data + filter := model.TimeSeriesDescriptionDataType{ + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + } + if _, err = evTimeSeries.GetDescriptionsForFilter(filter); err != nil { + return false, err + } + + // check if incentive table descriptions contains data for the required scope + filter2 := model.TariffDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + } + if _, err = evIncentiveTable.GetDescriptionsForFilter(filter2); err != nil { + return false, err + } + + return true, nil +} diff --git a/usecases/cem/cevc/usecase_test.go b/usecases/cem/cevc/usecase_test.go new file mode 100644 index 00000000..853d0894 --- /dev/null +++ b/usecases/cem/cevc/usecase_test.go @@ -0,0 +1,81 @@ +package cevc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *CEVCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *CEVCSuite) 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeCoordinatedEVCharging), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{2, 3, 4, 5, 6, 7, 8}, + }, + }, + }, + }, + } + + 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.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + timeDescData := &model.TimeSeriesDescriptionListDataType{ + TimeSeriesDescriptionData: []model.TimeSeriesDescriptionDataType{ + { + TimeSeriesId: util.Ptr(model.TimeSeriesIdType(0)), + TimeSeriesType: util.Ptr(model.TimeSeriesTypeTypeConstraints), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeTimeSeries, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeTimeSeriesDescriptionListData, timeDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.IncentiveTableDescriptionDataType{ + IncentiveTableDescription: []model.IncentiveTableDescriptionType{ + { + TariffDescription: &model.TariffDescriptionDataType{ + TariffId: util.Ptr(model.TariffIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeSimpleIncentiveTable), + }, + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIncentiveTable, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeIncentiveTableDescriptionData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/evcc/events.go b/usecases/cem/evcc/events.go new file mode 100644 index 00000000..2bea338c --- /dev/null +++ b/usecases/cem/evcc/events.go @@ -0,0 +1,199 @@ +package evcc + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemEVCC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.evConnected(payload) + return + } else if internal.IsEntityDisconnected(payload) { + e.evDisconnected(payload) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.evConfigurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.evConfigurationDataUpdate(payload) + case *model.DeviceDiagnosisOperatingStateType: + e.evOperatingStateDataUpdate(payload) + case *model.DeviceClassificationManufacturerDataType: + e.evManufacturerDataUpdate(payload) + case *model.ElectricalConnectionParameterDescriptionListDataType: + e.evElectricalParamerDescriptionUpdate(payload.Entity) + case *model.ElectricalConnectionPermittedValueSetListDataType: + e.evElectricalPermittedValuesUpdate(payload) + case *model.IdentificationListDataType: + e.evIdentificationDataUpdate(payload) + } +} + +// an EV was connected +func (e *CemEVCC) evConnected(payload spineapi.EventPayload) { + // initialise features, e.g. subscriptions, descriptions + if evDeviceClassification, err := client.NewDeviceClassification(e.LocalEntity, payload.Entity); err == nil { + if _, err := evDeviceClassification.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get manufacturer details + if _, err := evDeviceClassification.RequestManufacturerDetails(); err != nil { + logging.Log().Debug(err) + } + } + + if evDeviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, payload.Entity); err == nil { + if _, err := evDeviceConfiguration.Subscribe(); err != nil { + logging.Log().Debug(err) + } + // get ev configuration data + if _, err := evDeviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if evDeviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, payload.Entity); err == nil { + if _, err := evDeviceDiagnosis.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get device diagnosis state + if _, err := evDeviceDiagnosis.RequestState(); err != nil { + logging.Log().Debug(err) + } + } + + if evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, payload.Entity); err == nil { + if _, err := evElectricalConnection.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get electrical connection parameter descriptions + if _, err := evElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get electrical permitted values descriptions + if _, err := evElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log().Debug(err) + } + } + + if evIdentification, err := client.NewIdentification(e.LocalEntity, payload.Entity); err == nil { + if _, err := evIdentification.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get identification + if _, err := evIdentification.RequestValues(); err != nil { + logging.Log().Debug(err) + } + } + + e.EventCB(payload.Ski, payload.Device, payload.Entity, EvConnected) +} + +// an EV was disconnected +func (e *CemEVCC) evDisconnected(payload spineapi.EventPayload) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, EvDisconnected) +} + +// the configuration key description data of an EV was updated +func (e *CemEVCC) evConfigurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if evDeviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity); err == nil { + // key value descriptions received, now get the data + if _, err := evDeviceConfiguration.RequestKeyValues(); err != nil { + logging.Log().Error("Error getting configuration key values:", err) + } + } +} + +// the configuration key data of an EV was updated +func (e *CemEVCC) evConfigurationDataUpdate(payload spineapi.EventPayload) { + if dc, err := client.NewDeviceConfiguration(e.LocalEntity, payload.Entity); err == nil { + // Scenario 2 + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCommunicationStandard) + } + + // Scenario 3 + filter.KeyName = util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported) + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateAsymmetricChargingSupport) + } + } +} + +// the operating state of an EV was updated +func (e *CemEVCC) evOperatingStateDataUpdate(payload spineapi.EventPayload) { + if deviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, payload.Entity); err == nil { + if _, err := deviceDiagnosis.GetState(); err == nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIdentifications) + } + } +} + +// the identification data of an EV was updated +func (e *CemEVCC) evIdentificationDataUpdate(payload spineapi.EventPayload) { + if evIdentification, err := client.NewIdentification(e.LocalEntity, payload.Entity); err == nil { + // Scenario 4 + if evIdentification.CheckEventPayloadDataForFilter(payload.Data) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateIdentifications) + } + } +} + +// the manufacturer data of an EV was updated +func (e *CemEVCC) evManufacturerDataUpdate(payload spineapi.EventPayload) { + if evDeviceClassification, err := client.NewDeviceClassification(e.LocalEntity, payload.Entity); err == nil { + // Scenario 5 + if _, err := evDeviceClassification.GetManufacturerDetails(); err == nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateManufacturerData) + } + } +} + +// the electrical connection parameter description data of an EV was updated +func (e *CemEVCC) evElectricalParamerDescriptionUpdate(entity spineapi.EntityRemoteInterface) { + if evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity); err == nil { + if _, err := evElectricalConnection.RequestPermittedValueSets(); err != nil { + logging.Log().Error("Error getting electrical permitted values:", err) + } + } +} + +// the electrical connection permitted value sets data of an EV was updated +func (e *CemEVCC) evElectricalPermittedValuesUpdate(payload spineapi.EventPayload) { + if evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, payload.Entity); err == nil { + filter := model.ElectricalConnectionParameterDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if evElectricalConnection.CheckEventPayloadDataForFilter(payload.Data, filter) { + // Scenario 6 + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentLimits) + } + } +} diff --git a/usecases/cem/evcc/events_test.go b/usecases/cem/evcc/events_test.go new file mode 100644 index 00000000..09ffb2a8 --- /dev/null +++ b/usecases/cem/evcc/events_test.go @@ -0,0 +1,275 @@ +package evcc + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVCCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + var value model.DeviceDiagnosisOperatingStateType + payload.Data = &value + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceClassificationManufacturerDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.ElectricalConnectionParameterDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.ElectricalConnectionPermittedValueSetListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.IdentificationListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *EVCCSuite) Test_Failures() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.evConnected(payload) + + s.sut.evConfigurationDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.evElectricalParamerDescriptionUpdate(s.mockRemoteEntity) +} + +func (s *EVCCSuite) Test_evConfigurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.evConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{ + String: util.Ptr(model.DeviceConfigurationKeyValueStringTypeISO151182ED2), + }), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{ + Boolean: util.Ptr(false), + }), + }, + }, + } + + payload.Data = data + + s.sut.evConfigurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *EVCCSuite) Test_evOperatingStateDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evOperatingStateDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evOperatingStateDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evOperatingStateDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *EVCCSuite) Test_evIdentificationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evIdentificationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evIdentificationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.IdentificationListDataType{ + IdentificationData: []model.IdentificationDataType{ + { + IdentificationId: util.Ptr(model.IdentificationIdType(0)), + IdentificationType: util.Ptr(model.IdentificationTypeTypeEui48), + }, + { + IdentificationId: util.Ptr(model.IdentificationIdType(1)), + IdentificationType: util.Ptr(model.IdentificationTypeTypeEui48), + IdentificationValue: util.Ptr(model.IdentificationValueType("test")), + }, + }, + } + + payload.Data = data + s.sut.evIdentificationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *EVCCSuite) Test_evManufacturerDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evManufacturerDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evManufacturerDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceClassificationManufacturerDataType{ + BrandName: util.Ptr(model.DeviceClassificationStringType("test")), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evManufacturerDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *EVCCSuite) Test_evElectricalPermittedValuesUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{*model.NewScaledNumberType(0)}, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(6), + Max: model.NewScaledNumberType(16), + }, + }, + }, + }, + }, + }, + } + + payload.Data = permData + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/evcc/public.go b/usecases/cem/evcc/public.go new file mode 100644 index 00000000..e125e1d7 --- /dev/null +++ b/usecases/cem/evcc/public.go @@ -0,0 +1,280 @@ +package evcc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the current charge state of the EV +func (e *CemEVCC) ChargeState(entity spineapi.EntityRemoteInterface) (ucapi.EVChargeStateType, error) { + if entity == nil || entity.EntityType() != model.EntityTypeTypeEV { + return ucapi.EVChargeStateTypeUnplugged, nil + } + + evDeviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, entity) + if err != nil { + return ucapi.EVChargeStateTypeUnplugged, err + } + + diagnosisState, err := evDeviceDiagnosis.GetState() + if err != nil { + return ucapi.EVChargeStateTypeUnknown, err + } + + operatingState := diagnosisState.OperatingState + if operatingState == nil { + return ucapi.EVChargeStateTypeUnknown, api.ErrDataNotAvailable + } + + switch *operatingState { + case model.DeviceDiagnosisOperatingStateTypeNormalOperation: + return ucapi.EVChargeStateTypeActive, nil + case model.DeviceDiagnosisOperatingStateTypeStandby: + return ucapi.EVChargeStateTypePaused, nil + case model.DeviceDiagnosisOperatingStateTypeFailure: + return ucapi.EVChargeStateTypeError, nil + case model.DeviceDiagnosisOperatingStateTypeFinished: + return ucapi.EVChargeStateTypeFinished, nil + } + + return ucapi.EVChargeStateTypeUnknown, nil +} + +// return if an EV is connected +// +// this includes all required features and +// minimal data being available +func (e *CemEVCC) EVConnected(entity spineapi.EntityRemoteInterface) bool { + if entity == nil || entity.Device() == nil { + return false + } + + // getting current charge state should work + if _, err := e.ChargeState(entity); err != nil { + return false + } + + remoteDevice := e.LocalEntity.Device().RemoteDeviceForSki(entity.Device().Ski()) + if remoteDevice == nil { + return false + } + + // check if the device still has an entity assigned with the provided entities address + return remoteDevice.Entity(entity.Address().Entity) == entity +} + +func (e *CemEVCC) deviceConfigurationValueForKeyName( + entity spineapi.EntityRemoteInterface, + keyname model.DeviceConfigurationKeyNameType, + valueType model.DeviceConfigurationKeyValueTypeType) (*model.DeviceConfigurationKeyValueDataType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + evDeviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil { + return nil, api.ErrDataNotAvailable + } + + // check if device configuration descriptions has an communication standard key name + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + } + if _, err = evDeviceConfiguration.GetKeyValueDescriptionsForFilter(filter); err != nil { + return nil, err + } + + filter.ValueType = &valueType + data, err := evDeviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil { + return nil, err + } + + if data == nil { + return nil, api.ErrDataNotAvailable + } + + return data, nil +} + +// return the current communication standard type used to communicate between EVSE and EV +// +// if an EV is connected via IEC61851, no ISO15118 specific data can be provided! +// sometimes the connection starts with IEC61851 before it switches +// to ISO15118, and sometimes it falls back again. so the error return is +// never absolut for the whole connection time, except if the use case +// is not supported +// +// the values are not constant and can change due to communication problems, bugs, and +// sometimes communication starts with IEC61851 before it switches to ISO +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemEVCC) CommunicationStandard(entity spineapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error) { + unknown := UCEVCCCommunicationStandardUnknown + + if !e.IsCompatibleEntity(entity) { + return unknown, api.ErrNoCompatibleEntity + } + + data, err := e.deviceConfigurationValueForKeyName(entity, model.DeviceConfigurationKeyNameTypeCommunicationsStandard, model.DeviceConfigurationKeyValueTypeTypeString) + if err != nil || data == nil || data.Value == nil || data.Value.String == nil { + return unknown, api.ErrDataNotAvailable + } + + return *data.Value.String, nil +} + +// return if the EV supports asymmetric charging +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +func (e *CemEVCC) AsymmetricChargingSupport(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + return false, api.ErrNoCompatibleEntity + } + + data, err := e.deviceConfigurationValueForKeyName(entity, model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported, model.DeviceConfigurationKeyValueTypeTypeBoolean) + if err != nil || data == nil || data.Value == nil || data.Value.Boolean == nil { + return false, api.ErrDataNotAvailable + } + + return *data.Value.Boolean, nil +} + +// return the identifications of the currently connected EV or nil if not available +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemEVCC) Identifications(entity spineapi.EntityRemoteInterface) ([]ucapi.IdentificationItem, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + evIdentification, err := client.NewIdentification(e.LocalEntity, entity) + if err != nil { + return nil, api.ErrDataNotAvailable + } + + identifications, err := evIdentification.GetDataForFilter(model.IdentificationDataType{}) + if err != nil { + return nil, err + } + + var ids []ucapi.IdentificationItem + for _, identification := range identifications { + item := ucapi.IdentificationItem{} + + typ := identification.IdentificationType + if typ != nil { + item.ValueType = *typ + } + + value := identification.IdentificationValue + if value != nil { + item.Value = string(*value) + } + + ids = append(ids, item) + } + + return ids, nil +} + +// the manufacturer data of an EVSE +// returns deviceName, serialNumber, error +func (e *CemEVCC) ManufacturerData( + entity spineapi.EntityRemoteInterface, +) ( + api.ManufacturerData, + error, +) { + if !e.IsCompatibleEntity(entity) { + return api.ManufacturerData{}, api.ErrNoCompatibleEntity + } + + return internal.ManufacturerData(e.LocalEntity, entity) +} + +// return the minimum, maximum charging and, standby power of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemEVCC) ChargingPowerLimits(entity spineapi.EntityRemoteInterface) (float64, float64, float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0.0, 0.0, 0.0, api.ErrNoCompatibleEntity + } + + evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return 0.0, 0.0, 0.0, api.ErrDataNotAvailable + } + + filter := model.ElectricalConnectionParameterDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + elParamDesc, err := evElectricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(elParamDesc) == 0 || elParamDesc[0].ParameterId == nil { + return 0.0, 0.0, 0.0, api.ErrDataNotAvailable + } + + filter2 := model.ElectricalConnectionPermittedValueSetDataType{ + ParameterId: elParamDesc[0].ParameterId, + } + dataSet, err := evElectricalConnection.GetPermittedValueSetForFilter(filter2) + if err != nil || len(dataSet) == 0 || + dataSet[0].PermittedValueSet == nil || + len(dataSet[0].PermittedValueSet) != 1 || + dataSet[0].PermittedValueSet[0].Range == nil || + len(dataSet[0].PermittedValueSet[0].Range) != 1 { + return 0.0, 0.0, 0.0, api.ErrDataNotAvailable + } + + var minValue, maxValue, standByValue float64 + if dataSet[0].PermittedValueSet[0].Range[0].Min != nil { + minValue = dataSet[0].PermittedValueSet[0].Range[0].Min.GetValue() + } + if dataSet[0].PermittedValueSet[0].Range[0].Max != nil { + maxValue = dataSet[0].PermittedValueSet[0].Range[0].Max.GetValue() + } + if dataSet[0].PermittedValueSet[0].Value != nil && len(dataSet[0].PermittedValueSet[0].Value) > 0 { + standByValue = dataSet[0].PermittedValueSet[0].Value[0].GetValue() + } + + return minValue, maxValue, standByValue, nil +} + +// is the EV in sleep mode +// returns operatingState, lastErrorCode, error +func (e *CemEVCC) IsInSleepMode( + entity spineapi.EntityRemoteInterface, +) (bool, error) { + if !e.IsCompatibleEntity(entity) { + return false, api.ErrNoCompatibleEntity + } + + evseDeviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, entity) + if err != nil { + return false, err + } + + data, err := evseDeviceDiagnosis.GetState() + if err != nil { + return false, err + } + + if data.OperatingState != nil && + *data.OperatingState == model.DeviceDiagnosisOperatingStateTypeStandby { + return true, nil + } + + return false, nil +} diff --git a/usecases/cem/evcc/public_test.go b/usecases/cem/evcc/public_test.go new file mode 100644 index 00000000..f570aa58 --- /dev/null +++ b/usecases/cem/evcc/public_test.go @@ -0,0 +1,421 @@ +package evcc + +import ( + "testing" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVCCSuite) Test_ChargeState() { + data, err := s.sut.ChargeState(s.mockRemoteEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypeUnplugged, data) + + data, err = s.sut.ChargeState(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypeUnknown, data) + + stateData := &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypeActive, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypePaused, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeFailure), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypeError, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeFinished), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypeFinished, data) + + stateData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeInAlarm), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ChargeState(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), ucapi.EVChargeStateTypeUnknown, data) +} + +func (s *EVCCSuite) Test_EVConnected() { + data := s.sut.EVConnected(nil) + assert.Equal(s.T(), false, data) + + data = s.sut.EVConnected(s.mockRemoteEntity) + assert.Equal(s.T(), false, data) + + data = s.sut.EVConnected(s.evEntity) + assert.Equal(s.T(), false, data) + + stateData := &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, stateData, nil, nil) + assert.Nil(s.T(), fErr) + + data = s.sut.EVConnected(s.evEntity) + assert.Equal(s.T(), true, data) +} + +func (s *EVCCSuite) Test_EVCommunicationStandard() { + data, err := s.sut.CommunicationStandard(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + descData = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeCommunicationsStandard), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeString), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), UCEVCCCommunicationStandardUnknown, data) + + devData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + String: util.Ptr(model.DeviceConfigurationKeyValueStringTypeISO151182ED2), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, devData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CommunicationStandard(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), model.DeviceConfigurationKeyValueStringTypeISO151182ED2, data) +} + +func (s *EVCCSuite) Test_EVAsymmetricChargingSupport() { + data, err := s.sut.AsymmetricChargingSupport(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData = &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeAsymmetricChargingSupported), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeBoolean), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + devData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + Boolean: util.Ptr(true), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, devData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.AsymmetricChargingSupport(s.evEntity) + assert.Nil(s.T(), err) + assert.True(s.T(), data) +} + +func (s *EVCCSuite) Test_EVIdentification() { + data, err := s.sut.Identifications(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), []ucapi.IdentificationItem(nil), data) + + data, err = s.sut.Identifications(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), []ucapi.IdentificationItem(nil), data) + + data, err = s.sut.Identifications(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), []ucapi.IdentificationItem(nil), data) + + idData := &model.IdentificationListDataType{ + IdentificationData: []model.IdentificationDataType{ + { + IdentificationId: util.Ptr(model.IdentificationIdType(0)), + IdentificationType: util.Ptr(model.IdentificationTypeTypeEui64), + IdentificationValue: util.Ptr(model.IdentificationValueType("test")), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeIdentification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeIdentificationListData, idData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Identifications(s.evEntity) + assert.Nil(s.T(), err) + resultData := []ucapi.IdentificationItem{{Value: "test", ValueType: model.IdentificationTypeTypeEui64}} + assert.Equal(s.T(), resultData, data) +} + +func (s *EVCCSuite) Test_EVManufacturerData() { + _, err := s.sut.ManufacturerData(nil) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.evEntity) + assert.NotNil(s.T(), err) + + descData := &model.DeviceClassificationManufacturerDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := s.sut.ManufacturerData(s.evEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "", data.DeviceName) + assert.Equal(s.T(), "", data.SerialNumber) + + descData = &model.DeviceClassificationManufacturerDataType{ + DeviceName: util.Ptr(model.DeviceClassificationStringType("test")), + SerialNumber: util.Ptr(model.DeviceClassificationStringType("12345")), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ManufacturerData(s.evEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "test", data.DeviceName) + assert.Equal(s.T(), "12345", data.SerialNumber) + assert.Equal(s.T(), "", data.BrandName) +} + +func (s *EVCCSuite) Test_EVChargingPowerLimits() { + minData, maxData, standByData, err := s.sut.ChargingPowerLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, minData) + assert.Equal(s.T(), 0.0, maxData) + assert.Equal(s.T(), 0.0, standByData) + + minData, maxData, standByData, err = s.sut.ChargingPowerLimits(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, minData) + assert.Equal(s.T(), 0.0, maxData) + assert.Equal(s.T(), 0.0, standByData) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + minData, maxData, standByData, err = s.sut.ChargingPowerLimits(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, minData) + assert.Equal(s.T(), 0.0, maxData) + assert.Equal(s.T(), 0.0, standByData) + + type permittedStruct struct { + standByValue, expectedStandByValue float64 + minValue, expectedMinValue float64 + maxValue, expectedMaxValue float64 + } + + tests := []struct { + name string + permitted permittedStruct + }{ + { + "IEC 3 Phase", + permittedStruct{0.1, 0.1, 4287600, 4287600, 11433600, 11433600}, + }, + { + "ISO15118 VW", + permittedStruct{0.1, 0.1, 800, 800, 11433600, 11433600}, + }, + { + "ISO15118 Taycan", + permittedStruct{0.1, 0.1, 400, 400, 11433600, 11433600}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + dataSet := []model.ElectricalConnectionPermittedValueSetDataType{} + permittedData := []model.ScaledNumberSetType{} + item := model.ScaledNumberSetType{ + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(tc.permitted.minValue), + Max: model.NewScaledNumberType(tc.permitted.maxValue), + }, + }, + Value: []model.ScaledNumberType{*model.NewScaledNumberType(tc.permitted.standByValue)}, + } + permittedData = append(permittedData, item) + + permittedItem := model.ElectricalConnectionPermittedValueSetDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + } + + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + minData, maxData, standByData, err = s.sut.ChargingPowerLimits(s.evEntity) + assert.Nil(s.T(), err) + + assert.Nil(s.T(), err) + assert.Equal(s.T(), tc.permitted.expectedMinValue, minData) + assert.Equal(s.T(), tc.permitted.expectedMaxValue, maxData) + assert.Equal(s.T(), tc.permitted.expectedStandByValue, standByData) + }) + } +} + +func (s *EVCCSuite) Test_EVInSleepMode() { + data, err := s.sut.IsInSleepMode(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + data, err = s.sut.IsInSleepMode(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.DeviceDiagnosisStateDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsInSleepMode(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsInSleepMode(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/evcc/results.go b/usecases/cem/evcc/results.go new file mode 100644 index 00000000..51fd7be3 --- /dev/null +++ b/usecases/cem/evcc/results.go @@ -0,0 +1,57 @@ +package evcc + +import ( + "fmt" + + "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +func (e *CemEVCC) HandleResponse(responseMsg api.ResponseMessage) { + // before SPINE 1.3 the heartbeats are on the EVSE entity + if responseMsg.EntityRemote == nil || + (responseMsg.EntityRemote.EntityType() != model.EntityTypeTypeEV && + responseMsg.EntityRemote.EntityType() != model.EntityTypeTypeEVSE) { + return + } + + // handle errors coming from the remote EVSE entity + if responseMsg.FeatureLocal.Type() == model.FeatureTypeTypeDeviceDiagnosis { + e.handleResultDeviceDiagnosis(responseMsg) + } +} + +// Handle DeviceDiagnosis Results +func (e *CemEVCC) handleResultDeviceDiagnosis(responseMsg api.ResponseMessage) { + // is this an error for a heartbeat message? + if responseMsg.DeviceRemote == nil || + responseMsg.Data == nil { + return + } + + result, ok := responseMsg.Data.(*model.ResultDataType) + if !ok { + return + } + + if result.ErrorNumber == nil || + *result.ErrorNumber == model.ErrorNumberTypeNoError { + return + } + + // check if this is for a cached notify message + datagram, err := responseMsg.DeviceRemote.Sender().DatagramForMsgCounter(responseMsg.MsgCounterReference) + if err != nil { + return + } + + if len(datagram.Payload.Cmd) > 0 && + datagram.Payload.Cmd[0].DeviceDiagnosisHeartbeatData != nil { + // something is horribly wrong, disconnect and hope a new connection will fix it + errorText := fmt.Sprintf("Error Code: %d", result.ErrorNumber) + if result.Description != nil { + errorText = fmt.Sprintf("%s - %s", errorText, string(*result.Description)) + } + e.service.DisconnectSKI(responseMsg.DeviceRemote.Ski(), errorText) + } +} diff --git a/usecases/cem/evcc/results_test.go b/usecases/cem/evcc/results_test.go new file mode 100644 index 00000000..6b2a1df3 --- /dev/null +++ b/usecases/cem/evcc/results_test.go @@ -0,0 +1,72 @@ +package evcc + +import ( + "errors" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +func (s *EVCCSuite) Test_Results() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + localFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + errorMsg := spineapi.ResponseMessage{ + DeviceRemote: s.remoteDevice, + EntityRemote: s.evEntity, + FeatureLocal: localFeature, + Data: util.Ptr(model.MsgCounterType(0)), + } + s.sut.HandleResponse(errorMsg) + + errorMsg = spineapi.ResponseMessage{ + EntityRemote: s.evEntity, + FeatureLocal: localFeature, + Data: util.Ptr(model.MsgCounterType(0)), + } + s.sut.HandleResponse(errorMsg) + + errorMsg = spineapi.ResponseMessage{ + DeviceRemote: s.remoteDevice, + EntityRemote: s.mockRemoteEntity, + FeatureLocal: localFeature, + Data: &model.ResultDataType{ + ErrorNumber: util.Ptr(model.ErrorNumberTypeNoError), + }, + } + s.sut.HandleResponse(errorMsg) + + errorMsg.EntityRemote = s.evEntity + s.sut.HandleResponse(errorMsg) + + errorMsg.Data = &model.ResultDataType{ + ErrorNumber: util.Ptr(model.ErrorNumberTypeGeneralError), + Description: util.Ptr(model.DescriptionType("test error")), + } + errorMsg.MsgCounterReference = model.MsgCounterType(500) + + s.mockSender. + EXPECT(). + DatagramForMsgCounter(errorMsg.MsgCounterReference). + Return(model.DatagramType{}, errors.New("test")).Once() + + s.sut.HandleResponse(errorMsg) + + datagram := model.DatagramType{ + Payload: model.PayloadType{ + Cmd: []model.CmdType{ + { + DeviceDiagnosisHeartbeatData: &model.DeviceDiagnosisHeartbeatDataType{}, + }, + }, + }, + } + s.mockSender. + EXPECT(). + DatagramForMsgCounter(errorMsg.MsgCounterReference). + Return(datagram, nil).Once() + + s.sut.HandleResponse(errorMsg) +} diff --git a/usecases/cem/evcc/testhelper_test.go b/usecases/cem/evcc/testhelper_test.go new file mode 100644 index 00000000..38376032 --- /dev/null +++ b/usecases/cem/evcc/testhelper_test.go @@ -0,0 +1,209 @@ +package evcc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVCCSuite(t *testing.T) { + suite.Run(t, new(EVCCSuite)) +} + +type EVCCSuite struct { + suite.Suite + + sut *CemEVCC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockSender *spinemocks.SenderInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *EVCCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *EVCCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemEVCC(s.service, localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, s.mockSender, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + *spinemocks.SenderInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + mockSender := spinemocks.NewSenderInterface(t) + defaultMsgCounter := model.MsgCounterType(100) + mockSender. + EXPECT(). + Request(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(&defaultMsgCounter, nil). + Maybe() + mockSender. + EXPECT(). + Subscribe(mock.Anything, mock.Anything, mock.Anything). + Return(&defaultMsgCounter, nil). + Maybe() + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, mockSender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + {model.FeatureTypeTypeIdentification, + []model.FunctionType{ + model.FunctionTypeIdentificationListData, + }, + }, + {model.FeatureTypeTypeDeviceClassification, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionDescriptionListData, + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisStateData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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, mockSender, entities +} diff --git a/usecases/cem/evcc/types.go b/usecases/cem/evcc/types.go new file mode 100644 index 00000000..ee4f1bca --- /dev/null +++ b/usecases/cem/evcc/types.go @@ -0,0 +1,71 @@ +package evcc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/spine-go/model" +) + +// value if the UCEVCC communication standard is unknown +const ( + UCEVCCCommunicationStandardUnknown model.DeviceConfigurationKeyValueStringType = "unknown" +) + +const ( + + // An EV was connected + // + // Use Case EVCC, Scenario 1 + EvConnected api.EventType = "cem-evcc-EvConnected" + + // An EV was disconnected + // + // Note: The ev entity is no longer connected to the device! + // + // Use Case EVCC, Scenario 8 + EvDisconnected api.EventType = "cem-evcc-EvDisconnected" + + // EV charge state data was updated + // + // Use `ChargeState` to get the current data + DataUpdateChargeState api.EventType = "cem-evcc-DataUpdateChargeState" + + // EV communication standard data was updated + // + // Use `CommunicationStandard` to get the current data + // + // Use Case EVCC, Scenario 2 + DataUpdateCommunicationStandard api.EventType = "cem-evcc-DataUpdateCommunicationStandard" + + // EV asymmetric charging data was updated + // + // Use `AsymmetricChargingSupport` to get the current data + DataUpdateAsymmetricChargingSupport api.EventType = "cem-evcc-DataUpdateAsymmetricChargingSupport" + + // EV identificationdata was updated + // + // Use `Identifications` to get the current data + // + // Use Case EVCC, Scenario 4 + DataUpdateIdentifications api.EventType = "cem-evcc-DataUpdateIdentifications" + + // EV manufacturer data was updated + // + // Use `ManufacturerData` to get the current data + // + // Use Case EVCC, Scenario 5 + DataUpdateManufacturerData api.EventType = "cem-evcc-DataUpdateManufacturerData" + + // EV charging power limits + // + // Use `ChargingPowerLimits` to get the current data + // + // Use Case EVCC, Scenario 6 + DataUpdateCurrentLimits api.EventType = "cem-evcc-DataUpdateCurrentLimits" + + // EV permitted power limits updated + // + // Use `IsInSleepMode` to get the current data + // + // Use Case EVCC, Scenario 7 + DataUpdateIsInSleepMode api.EventType = "cem-evcc-DataUpdateIsInSleepMode" +) diff --git a/usecases/cem/evcc/usecase.go b/usecases/cem/evcc/usecase.go new file mode 100644 index 00000000..c25784d9 --- /dev/null +++ b/usecases/cem/evcc/usecase.go @@ -0,0 +1,87 @@ +package evcc + +import ( + "github.com/enbility/eebus-go/api" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type CemEVCC struct { + *usecase.UseCaseBase + + service api.ServiceInterface +} + +var _ ucapi.CemEVCCInterface = (*CemEVCC)(nil) + +func NewCemEVCC( + service api.ServiceInterface, + localEntity spineapi.EntityLocalInterface, + eventCB api.EntityEventCallback, +) *CemEVCC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeEVCommissioningAndConfiguration, + "1.0.1", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, + eventCB, + validEntityTypes, + ) + + uc := &CemEVCC{ + UseCaseBase: usecase, + service: service, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemEVCC) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeIdentification, + model.FeatureTypeTypeDeviceClassification, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeDeviceDiagnosis, + } + for _, feature := range clientFeatures { + f := e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + f.AddResultCallback(e.HandleResponse) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemEVCC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeEV, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1, 2, 3, 8}, + []model.FeatureTypeType{model.FeatureTypeTypeDeviceConfiguration}, + ) { + return false, nil + } + + return true, nil +} diff --git a/usecases/cem/evcc/usecase_test.go b/usecases/cem/evcc/usecase_test.go new file mode 100644 index 00000000..2a21dd89 --- /dev/null +++ b/usecases/cem/evcc/usecase_test.go @@ -0,0 +1,67 @@ +package evcc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVCCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *EVCCSuite) 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVCommissioningAndConfiguration), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 3, 4, 5, 6, 7, 8}, + }, + }, + }, + }, + } + + 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData = &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVCommissioningAndConfiguration), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7, 8}, + }, + }, + }, + }, + } + + fErr = nodeFeature.UpdateData(model.FunctionTypeNodeManagementUseCaseData, ucData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/evcem/events.go b/usecases/cem/evcem/events.go new file mode 100644 index 00000000..65d97465 --- /dev/null +++ b/usecases/cem/evcem/events.go @@ -0,0 +1,128 @@ +package evcem + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemEVCEM) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + switch payload.Data.(type) { + case *model.ElectricalConnectionDescriptionListDataType: + e.evElectricalConnectionDescriptionDataUpdate(payload) + case *model.MeasurementDescriptionListDataType: + e.evMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.evMeasurementDataUpdate(payload) + } +} + +// an EV was connected +func (e *CemEVCEM) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + + if evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity); err == nil { + if _, err := evElectricalConnection.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get electrical connection descriptions + if _, err := evElectricalConnection.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get electrical connection parameter descriptions + if _, err := evElectricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Debug(err) + } + } + + if evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if _, err := evMeasurement.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get measurement descriptions + if _, err := evMeasurement.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get measurement constraints + if _, err := evMeasurement.RequestConstraints(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the electrical connection description data of an EV was updated +func (e *CemEVCEM) evElectricalConnectionDescriptionDataUpdate(payload spineapi.EventPayload) { + if payload.Data == nil { + return + } + + data, ok := payload.Data.(*model.ElectricalConnectionDescriptionListDataType) + if !ok { + return + } + + for _, item := range data.ElectricalConnectionDescriptionData { + if item.ElectricalConnectionId != nil && item.AcConnectedPhases != nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePhasesConnected) + return + } + } +} + +// the measurement description data of an EV was updated +func (e *CemEVCEM) evMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + // get measurement values + if _, err := evMeasurement.RequestData(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the measurement data of an EV was updated +func (e *CemEVCEM) evMeasurementDataUpdate(payload spineapi.EventPayload) { + if evMeasurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + // Scenario 1 + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + } + if evMeasurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentPerPhase) + } + + // Scenario 2 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACPower) + if evMeasurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerPerPhase) + } + + // Scenario 3 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeCharge) + if evMeasurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyCharged) + } + } +} diff --git a/usecases/cem/evcem/events_test.go b/usecases/cem/evcem/events_test.go new file mode 100644 index 00000000..507eca0a --- /dev/null +++ b/usecases/cem/evcem/events_test.go @@ -0,0 +1,139 @@ +package evcem + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVCEMSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + 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 = util.Ptr(model.ElectricalConnectionDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *EVCEMSuite) Test_Failures() { + s.sut.evConnected(s.mockRemoteEntity) + + s.sut.evMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *EVCEMSuite) Test_evElectricalConnectionDescriptionDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + payload.Data = s.evEntity + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + + descData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{}, + } + + payload.Data = descData + + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData = &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + AcConnectedPhases: util.Ptr(uint(1)), + }, + }, + } + + payload.Data = descData + + s.sut.evElectricalConnectionDescriptionDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *EVCEMSuite) Test_evMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(200), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(3000), + }, + }, + } + payload.Data = data + + s.sut.evMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/evcem/public.go b/usecases/cem/evcem/public.go new file mode 100644 index 00000000..0c9b5e01 --- /dev/null +++ b/usecases/cem/evcem/public.go @@ -0,0 +1,203 @@ +package evcem + +import ( + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the number of ac connected phases of the EV or 0 if it is unknown +func (e *CemEVCEM) PhasesConnected(entity spineapi.EntityRemoteInterface) (uint, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + evElectricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return 0, api.ErrDataNotAvailable + } + + data, err := evElectricalConnection.GetDescriptionsForFilter(model.ElectricalConnectionDescriptionDataType{}) + if err != nil || len(data) == 0 { + return 0, api.ErrDataNotAvailable + } + + for _, item := range data { + if item.ElectricalConnectionId != nil && item.AcConnectedPhases != nil { + return *item.AcConnectedPhases, nil + } + } + + // default to 0 if the value is not available + return 0, nil +} + +// return the last current measurement for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemEVCEM) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity) + evElectricalConnection, err2 := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil || err2 != nil { + return nil, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + } + data, err := evMeasurement.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return nil, api.ErrDataNotAvailable + } + + var result []float64 + refetch := true + compare := time.Now().Add(-1 * time.Minute) + + for _, phase := range internal.PhaseNameMapping { + for _, item := range data { + if item.Value == nil { + continue + } + + filter := model.ElectricalConnectionParameterDescriptionDataType{ + MeasurementId: item.MeasurementId, + } + elParam, err := evElectricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(elParam) == 0 || + elParam[0].AcMeasuredPhases == nil || *elParam[0].AcMeasuredPhases != phase { + continue + } + + phaseValue := item.Value.GetValue() + result = append(result, phaseValue) + + if item.Timestamp != nil { + if timestamp, err := item.Timestamp.GetTime(); err == nil { + refetch = timestamp.Before(compare) + } + } + } + } + + // if there was no timestamp provided or the time for the last value + // is older than 1 minute, send a read request + if refetch { + _, _ = evMeasurement.RequestData() + } + + return result, nil +} + +// return the last power measurement for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemEVCEM) PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity) + evElectricalConnection, err2 := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil || err2 != nil { + return nil, err + } + + var data []model.MeasurementDataType + + powerAvailable := true + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + } + data, err = evMeasurement.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + powerAvailable = false + + // If power is not provided, fall back to power calculations via currents + filter.MeasurementType = util.Ptr(model.MeasurementTypeTypeCurrent) + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACCurrent) + data, err = evMeasurement.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return nil, api.ErrDataNotAvailable + } + } + + var result []float64 + + for _, phase := range internal.PhaseNameMapping { + for _, item := range data { + if item.Value == nil { + continue + } + + filter := model.ElectricalConnectionParameterDescriptionDataType{ + MeasurementId: item.MeasurementId, + } + elParam, err := evElectricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(elParam) == 0 || + *elParam[0].AcMeasuredPhases != phase { + continue + } + + phaseValue := item.Value.GetValue() + if !powerAvailable { + phaseValue *= e.service.Configuration().Voltage() + } + + result = append(result, phaseValue) + } + } + + return result, nil +} + +// return the charged energy measurement in Wh of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemEVCEM) EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + } + data, err := evMeasurement.GetDataForFilter(filter) + if err != nil || len(data) == 0 { + return 0, api.ErrDataNotAvailable + } + + // we assume there is only one result + value := data[0].Value + if value == nil { + return 0, api.ErrDataNotAvailable + } + + return value.GetValue(), err +} diff --git a/usecases/cem/evcem/public_test.go b/usecases/cem/evcem/public_test.go new file mode 100644 index 00000000..0ede7f8d --- /dev/null +++ b/usecases/cem/evcem/public_test.go @@ -0,0 +1,289 @@ +package evcem + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVCEMSuite) Test_EVConnectedPhases() { + data, err := s.sut.PhasesConnected(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), uint(0), data) + + data, err = s.sut.PhasesConnected(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), uint(0), data) + + descData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PhasesConnected(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), uint(0), data) + + descData = &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + AcConnectedPhases: util.Ptr(uint(1)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PhasesConnected(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), uint(1), data) +} + +func (s *EVCEMSuite) Test_EVCurrentPerPhase() { + data, err := s.sut.CurrentPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.CurrentPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + paramDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data[0]) +} + +func (s *EVCEMSuite) Test_EVPowerPerPhase_Power() { + data, err := s.sut.PowerPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + paramDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 80.0, data[0]) +} + +func (s *EVCEMSuite) Test_EVPowerPerPhase_Current() { + data, err := s.sut.PowerPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + paramDesc := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature = s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2300.0, data[0]) +} + +func (s *EVCEMSuite) Test_EVChargedEnergy() { + data, err := s.sut.EnergyCharged(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyCharged(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 80.0, data) +} diff --git a/usecases/cem/evcem/testhelper_test.go b/usecases/cem/evcem/testhelper_test.go new file mode 100644 index 00000000..8475a981 --- /dev/null +++ b/usecases/cem/evcem/testhelper_test.go @@ -0,0 +1,182 @@ +package evcem + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVCEMSuite(t *testing.T) { + suite.Run(t, new(EVCEMSuite)) +} + +type EVCEMSuite struct { + suite.Suite + + sut *CemEVCEM + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *EVCEMSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *EVCEMSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemEVCEM(s.service, localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionDescriptionListData, + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + { + model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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 +} diff --git a/usecases/cem/evcem/types.go b/usecases/cem/evcem/types.go new file mode 100644 index 00000000..f7909013 --- /dev/null +++ b/usecases/cem/evcem/types.go @@ -0,0 +1,33 @@ +package evcem + +import "github.com/enbility/eebus-go/api" + +const ( + // EV number of connected phases data updated + // + // Use `PhasesConnected` to get the current data + // + // Use Case EVCEM, Scenario 1 + DataUpdatePhasesConnected api.EventType = "cem-evcem-DataUpdatePhasesConnected" + + // EV current measurement data updated + // + // Use `CurrentPerPhase` to get the current data + // + // Use Case EVCEM, Scenario 1 + DataUpdateCurrentPerPhase api.EventType = "cem-evcem-DataUpdateCurrentPerPhase" + + // EV power measurement data updated + // + // Use `PowerPerPhase` to get the current data + // + // Use Case EVCEM, Scenario 2 + DataUpdatePowerPerPhase api.EventType = "cem-evcem-DataUpdatePowerPerPhase" + + // EV charging energy measurement data updated + // + // Use `EnergyCharged` to get the current data + // + // Use Case EVCEM, Scenario 3 + DataUpdateEnergyCharged api.EventType = "cem-evcem-DataUpdateEnergyCharged" +) diff --git a/usecases/cem/evcem/usecase.go b/usecases/cem/evcem/usecase.go new file mode 100644 index 00000000..c7624cbb --- /dev/null +++ b/usecases/cem/evcem/usecase.go @@ -0,0 +1,78 @@ +package evcem + +import ( + "github.com/enbility/eebus-go/api" + ucapi "github.com/enbility/eebus-go/usecases/api" + usecase "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type CemEVCEM struct { + *usecase.UseCaseBase + + service api.ServiceInterface +} + +var _ ucapi.CemEVCEMInterface = (*CemEVCEM)(nil) + +func NewCemEVCEM(service api.ServiceInterface, localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemEVCEM { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeMeasurementOfElectricityDuringEVCharging, + "1.0.1", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3}, + eventCB, + validEntityTypes) + + uc := &CemEVCEM{ + UseCaseBase: usecase, + service: service, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemEVCEM) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemEVCEM) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeEV, + e.UseCaseName, + nil, + nil, + ) { + return false, nil + } + + return true, nil +} diff --git a/usecases/cem/evcem/usecase_test.go b/usecases/cem/evcem/usecase_test.go new file mode 100644 index 00000000..264e8d3c --- /dev/null +++ b/usecases/cem/evcem/usecase_test.go @@ -0,0 +1,45 @@ +package evcem + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVCEMSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *EVCEMSuite) 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeMeasurementOfElectricityDuringEVCharging), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/evsecc/events.go b/usecases/cem/evsecc/events.go new file mode 100644 index 00000000..7fd09fb8 --- /dev/null +++ b/usecases/cem/evsecc/events.go @@ -0,0 +1,73 @@ +package evsecc + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// handle SPINE events +func (e *CemEVSECC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EVSE entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.evseConnected(payload) + return + } else if internal.IsEntityDisconnected(payload) { + e.evseDisconnected(payload) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceClassificationManufacturerDataType: + e.evseManufacturerDataUpdate(payload) + case *model.DeviceDiagnosisStateDataType: + e.evseStateUpdate(payload) + } +} + +// an EVSE was connected +func (e *CemEVSECC) evseConnected(payload spineapi.EventPayload) { + if evseDeviceClassification, err := client.NewDeviceClassification(e.LocalEntity, payload.Entity); err == nil { + _, _ = evseDeviceClassification.RequestManufacturerDetails() + } + + if evseDeviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, payload.Entity); err == nil { + _, _ = evseDeviceDiagnosis.RequestState() + } + + e.EventCB(payload.Ski, payload.Device, payload.Entity, EvseConnected) +} + +// an EVSE was disconnected +func (e *CemEVSECC) evseDisconnected(payload spineapi.EventPayload) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, EvseDisconnected) +} + +// the manufacturer Data of an EVSE was updated +func (e *CemEVSECC) evseManufacturerDataUpdate(payload spineapi.EventPayload) { + if evDeviceClassification, err := client.NewDeviceClassification(e.LocalEntity, payload.Entity); err == nil { + if _, err := evDeviceClassification.GetManufacturerDetails(); err == nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateManufacturerData) + } + } +} + +// the operating State of an EVSE was updated +func (e *CemEVSECC) evseStateUpdate(payload spineapi.EventPayload) { + if evDeviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, payload.Entity); err == nil { + if _, err := evDeviceDiagnosis.GetState(); err == nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateOperatingState) + } + } +} diff --git a/usecases/cem/evsecc/events_test.go b/usecases/cem/evsecc/events_test.go new file mode 100644 index 00000000..77450a74 --- /dev/null +++ b/usecases/cem/evsecc/events_test.go @@ -0,0 +1,92 @@ +package evsecc + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVSECCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evseEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDeviceChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = util.Ptr(model.DeviceClassificationManufacturerDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceDiagnosisStateDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *EVSECCSuite) Test_evseManufacturerDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evseManufacturerDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evseEntity + s.sut.evseManufacturerDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceClassificationManufacturerDataType{ + BrandName: util.Ptr(model.DeviceClassificationStringType("test")), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evseManufacturerDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *EVSECCSuite) Test_evseStateUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evseStateUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evseEntity + s.sut.evseStateUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeNormalOperation), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, data, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evseStateUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/evsecc/public.go b/usecases/cem/evsecc/public.go new file mode 100644 index 00000000..fac0a380 --- /dev/null +++ b/usecases/cem/evsecc/public.go @@ -0,0 +1,58 @@ +package evsecc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// the manufacturer data of an EVSE +// returns deviceName, serialNumber, error +func (e *CemEVSECC) ManufacturerData( + entity spineapi.EntityRemoteInterface, +) ( + api.ManufacturerData, + error, +) { + if !e.IsCompatibleEntity(entity) { + return api.ManufacturerData{}, api.ErrNoCompatibleEntity + } + + return internal.ManufacturerData(e.LocalEntity, entity) +} + +// the operating state data of an EVSE +// returns operatingState, lastErrorCode, error +func (e *CemEVSECC) OperatingState( + entity spineapi.EntityRemoteInterface, +) ( + model.DeviceDiagnosisOperatingStateType, string, error, +) { + operatingState := model.DeviceDiagnosisOperatingStateTypeNormalOperation + lastErrorCode := "" + + if !e.IsCompatibleEntity(entity) { + return operatingState, lastErrorCode, api.ErrNoCompatibleEntity + } + + evseDeviceDiagnosis, err := client.NewDeviceDiagnosis(e.LocalEntity, entity) + if err != nil { + return operatingState, lastErrorCode, err + } + + data, err := evseDeviceDiagnosis.GetState() + if err != nil { + return operatingState, lastErrorCode, err + } + + if data.OperatingState != nil { + operatingState = *data.OperatingState + } + if data.LastErrorCode != nil { + lastErrorCode = string(*data.LastErrorCode) + } + + return operatingState, lastErrorCode, nil +} diff --git a/usecases/cem/evsecc/public_test.go b/usecases/cem/evsecc/public_test.go new file mode 100644 index 00000000..c6e4d862 --- /dev/null +++ b/usecases/cem/evsecc/public_test.go @@ -0,0 +1,86 @@ +package evsecc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVSECCSuite) Test_EVSEManufacturerData() { + _, err := s.sut.ManufacturerData(nil) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.ManufacturerData(s.evseEntity) + assert.NotNil(s.T(), err) + + descData := &model.DeviceClassificationManufacturerDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := s.sut.ManufacturerData(s.evseEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "", data.DeviceName) + assert.Equal(s.T(), "", data.SerialNumber) + + descData = &model.DeviceClassificationManufacturerDataType{ + DeviceName: util.Ptr(model.DeviceClassificationStringType("test")), + SerialNumber: util.Ptr(model.DeviceClassificationStringType("12345")), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.ManufacturerData(s.evseEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "test", data.DeviceName) + assert.Equal(s.T(), "12345", data.SerialNumber) + assert.Equal(s.T(), "", data.BrandName) +} + +func (s *EVSECCSuite) Test_EVSEOperatingState() { + data, errCode, err := s.sut.OperatingState(nil) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.Nil(s.T(), nil, err) + + data, errCode, err = s.sut.OperatingState(s.mockRemoteEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.NotNil(s.T(), err) + + data, errCode, err = s.sut.OperatingState(s.evseEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.NotNil(s.T(), err) + + descData := &model.DeviceDiagnosisStateDataType{} + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evseEntity, model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, errCode, err = s.sut.OperatingState(s.evseEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeNormalOperation, data) + assert.Equal(s.T(), "", errCode) + assert.Nil(s.T(), err) + + descData = &model.DeviceDiagnosisStateDataType{ + OperatingState: util.Ptr(model.DeviceDiagnosisOperatingStateTypeStandby), + LastErrorCode: util.Ptr(model.LastErrorCodeType("error")), + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceDiagnosisStateData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, errCode, err = s.sut.OperatingState(s.evseEntity) + assert.Equal(s.T(), model.DeviceDiagnosisOperatingStateTypeStandby, data) + assert.Equal(s.T(), "error", errCode) + assert.Nil(s.T(), err) +} diff --git a/usecases/cem/evsecc/testhelper_test.go b/usecases/cem/evsecc/testhelper_test.go new file mode 100644 index 00000000..086e2414 --- /dev/null +++ b/usecases/cem/evsecc/testhelper_test.go @@ -0,0 +1,181 @@ +package evsecc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVSECCSuite(t *testing.T) { + suite.Run(t, new(EVSECCSuite)) +} + +type EVSECCSuite struct { + suite.Suite + + sut *CemEVSECC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evseEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *EVSECCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *EVSECCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemEVSECC(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evseEntity = entities[0] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeDeviceClassification, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisStateData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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 +} diff --git a/usecases/cem/evsecc/types.go b/usecases/cem/evsecc/types.go new file mode 100644 index 00000000..709bda21 --- /dev/null +++ b/usecases/cem/evsecc/types.go @@ -0,0 +1,29 @@ +package evsecc + +import "github.com/enbility/eebus-go/api" + +const ( + // An EVSE was connected + EvseConnected api.EventType = "cem-evsecc-EvseConnected" + + // An EVSE was disconnected + EvseDisconnected api.EventType = "cem-evsecc-EvseDisconnected" + + // EVSE manufacturer data was updated + // + // Use `ManufacturerData` to get the current data + // + // Use Case EVSECC, Scenario 1 + // + // The entity of the message is the entity of the EVSE + DataUpdateManufacturerData api.EventType = "cem-evsecc-DataUpdateManufacturerData" + + // EVSE operation state was updated + // + // Use `OperatingState` to get the current data + // + // Use Case EVSECC, Scenario 2 + // + // The entity of the message is the entity of the EVSE + DataUpdateOperatingState api.EventType = "cem-evsecc-DataUpdateOperatingState" +) diff --git a/usecases/cem/evsecc/usecase.go b/usecases/cem/evsecc/usecase.go new file mode 100644 index 00000000..8c39930a --- /dev/null +++ b/usecases/cem/evsecc/usecase.go @@ -0,0 +1,85 @@ +package evsecc + +import ( + "github.com/enbility/eebus-go/api" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type CemEVSECC struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemEVSECCInterface = (*CemEVSECC)(nil) + +func NewCemEVSECC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemEVSECC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEVSE, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeEVSECommissioningAndConfiguration, + "1.0.1", + "release", + []model.UseCaseScenarioSupportType{1, 2}, + eventCB, + validEntityTypes) + + uc := &CemEVSECC{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemEVSECC) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceClassification, + model.FeatureTypeTypeDeviceDiagnosis, + } + + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemEVSECC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeEVSE, + e.UseCaseName, + []model.UseCaseScenarioSupportType{2}, + []model.FeatureTypeType{model.FeatureTypeTypeDeviceDiagnosis}, + ) { + // Workaround for the Porsche Mobile Charger Connect that falsely reports + // the usecase to be on the EV actor + if !entity.Device().VerifyUseCaseScenariosAndFeaturesSupport( + model.UseCaseActorTypeEV, + e.UseCaseName, + []model.UseCaseScenarioSupportType{2}, + []model.FeatureTypeType{model.FeatureTypeTypeDeviceDiagnosis}, + ) { + return false, nil + } + } + + return true, nil +} diff --git a/usecases/cem/evsecc/usecase_test.go b/usecases/cem/evsecc/usecase_test.go new file mode 100644 index 00000000..0aa7363a --- /dev/null +++ b/usecases/cem/evsecc/usecase_test.go @@ -0,0 +1,45 @@ +package evsecc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVSECCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *EVSECCSuite) 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.evseEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVSECommissioningAndConfiguration), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{2}, + }, + }, + }, + }, + } + + 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.evseEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/evsoc/events.go b/usecases/cem/evsoc/events.go new file mode 100644 index 00000000..14d41f26 --- /dev/null +++ b/usecases/cem/evsoc/events.go @@ -0,0 +1,69 @@ +package evsoc + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemEVSOC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + 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.MeasurementListDataType: + e.evMeasurementDataUpdate(payload) + } +} + +// an EV was connected +func (e *CemEVSOC) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if _, err := evMeasurement.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + // get measurement descriptions + if _, err := evMeasurement.RequestDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get measurement constraints + if _, err := evMeasurement.RequestConstraints(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the measurement data of an EV was updated +func (e *CemEVSOC) evMeasurementDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if evMeasurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + } + if evMeasurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateStateOfCharge) + } + } +} diff --git a/usecases/cem/evsoc/events_test.go b/usecases/cem/evsoc/events_test.go new file mode 100644 index 00000000..7c56c964 --- /dev/null +++ b/usecases/cem/evsoc/events_test.go @@ -0,0 +1,99 @@ +package evsoc + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVSOCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + 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 = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *EVSOCSuite) Test_Failures() { + s.sut.evConnected(s.mockRemoteEntity) +} + +func (s *EVSOCSuite) Test_evMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.eventCalled = false + s.sut.evMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfHealth), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + ScopeType: util.Ptr(model.ScopeTypeTypeTravelRange), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.eventCalled = false + + s.sut.evMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(200), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(3000), + }, + }, + } + + payload.Data = data + s.eventCalled = false + + s.sut.evMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/evsoc/public.go b/usecases/cem/evsoc/public.go new file mode 100644 index 00000000..b1588970 --- /dev/null +++ b/usecases/cem/evsoc/public.go @@ -0,0 +1,39 @@ +package evsoc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the last known SoC of the connected EV +// +// only works with a current ISO15118-2 with VAS or ISO15118-20 +// communication between EVSE and EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemEVSOC) StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil || evMeasurement == nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + } + result, err := evMeasurement.GetDataForFilter(filter) + if err != nil || len(result) == 0 || result[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + return result[0].Value.GetValue(), nil +} diff --git a/usecases/cem/evsoc/public_test.go b/usecases/cem/evsoc/public_test.go new file mode 100644 index 00000000..1881eb96 --- /dev/null +++ b/usecases/cem/evsoc/public_test.go @@ -0,0 +1,91 @@ +package evsoc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVSOCSuite) Test_StateOfCharge() { + data, err := s.sut.StateOfCharge(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVStateOfCharge), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + 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.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measDesc := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, measDesc, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(80), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 80.0, data) +} diff --git a/usecases/cem/evsoc/testhelper_test.go b/usecases/cem/evsoc/testhelper_test.go new file mode 100644 index 00000000..991fd00a --- /dev/null +++ b/usecases/cem/evsoc/testhelper_test.go @@ -0,0 +1,182 @@ +package evsoc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestEVSOCSuite(t *testing.T) { + suite.Run(t, new(EVSOCSuite)) +} + +type EVSOCSuite struct { + suite.Suite + + sut *CemEVSOC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *EVSOCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *EVSOCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemEVSOC(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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 +} diff --git a/usecases/cem/evsoc/types.go b/usecases/cem/evsoc/types.go new file mode 100644 index 00000000..f11719a4 --- /dev/null +++ b/usecases/cem/evsoc/types.go @@ -0,0 +1,12 @@ +package evsoc + +import "github.com/enbility/eebus-go/api" + +const ( + // EV state of charge data was updated + // + // Use `StateOfCharge` to get the current data + // + // Use Case EVSOC, Scenario 1 + DataUpdateStateOfCharge api.EventType = "ucevsoc-DataUpdateStateOfCharge" +) diff --git a/usecases/cem/evsoc/usecase.go b/usecases/cem/evsoc/usecase.go new file mode 100644 index 00000000..57a83c30 --- /dev/null +++ b/usecases/cem/evsoc/usecase.go @@ -0,0 +1,96 @@ +package evsoc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + usecase "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CemEVSOC struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemEVSOCInterface = (*CemEVSOC)(nil) + +func NewCemEVSOC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemEVSOC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeEVStateOfCharge, + "1.0.0", + "RC1", + []model.UseCaseScenarioSupportType{1}, + eventCB, + validEntityTypes, + ) + + uc := &CemEVSOC{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemEVSOC) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +func (e *CemEVSOC) UpdateUseCaseAvailability(available bool) { + e.LocalEntity.SetUseCaseAvailability(model.UseCaseActorTypeCEM, e.UseCaseName, available) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemEVSOC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeEV, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1}, + []model.FeatureTypeType{model.FeatureTypeTypeMeasurement}, + ) { + return false, nil + } + + // check for required features + evMeasurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil || evMeasurement == nil { + return false, api.ErrFunctionNotSupported + } + + // check if measurement description contains an element with scope SOC + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + } + if data, err := evMeasurement.GetDescriptionsForFilter(filter); data == nil || err != nil { + return false, api.ErrNoCompatibleEntity + } + + return true, nil +} diff --git a/usecases/cem/evsoc/usecase_test.go b/usecases/cem/evsoc/usecase_test.go new file mode 100644 index 00000000..241bfbb7 --- /dev/null +++ b/usecases/cem/evsoc/usecase_test.go @@ -0,0 +1,62 @@ +package evsoc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *EVSOCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *EVSOCSuite) 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeEVStateOfCharge), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + 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.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/opev/events.go b/usecases/cem/opev/events.go new file mode 100644 index 00000000..def409af --- /dev/null +++ b/usecases/cem/opev/events.go @@ -0,0 +1,119 @@ +package opev + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemOPEV) HandleEvent(payload spineapi.EventPayload) { + // only about events from an EV entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.evConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.ElectricalConnectionPermittedValueSetListDataType: + e.evElectricalPermittedValuesUpdate(payload) + case *model.LoadControlLimitDescriptionListDataType: + e.evLoadControlLimitDescriptionDataUpdate(payload.Entity) + case *model.LoadControlLimitListDataType: + e.evLoadControlLimitDataUpdate(payload) + } +} + +// an EV was connected +func (e *CemOPEV) evConnected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if evLoadControl, err := client.NewLoadControl(e.LocalEntity, entity); err == nil { + if _, err := evLoadControl.Subscribe(); err != nil { + logging.Log().Debug(err) + } + + if _, err := evLoadControl.Bind(); err != nil { + logging.Log().Debug(err) + } + + // get descriptions + if _, err := evLoadControl.RequestLimitDescriptions(); err != nil { + logging.Log().Debug(err) + } + + // get constraints + if _, err := evLoadControl.RequestLimitConstraints(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data of an EV was updated +func (e *CemOPEV) evLoadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if evLoadControl, err := client.NewLoadControl(e.LocalEntity, entity); err == nil { + // get values + if _, err := evLoadControl.RequestLimitData(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data of an EV was updated +func (e *CemOPEV) evLoadControlLimitDataUpdate(payload spineapi.EventPayload) { + lc, err := client.NewLoadControl(e.LocalEntity, payload.Entity) + if err != nil { + return + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + ScopeType: util.Ptr(model.ScopeTypeTypeOverloadProtection), + } + if lc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the electrical connection permitted value sets data of an EV was updated +func (e *CemOPEV) evElectricalPermittedValuesUpdate(payload spineapi.EventPayload) { + if ec, err := client.NewElectricalConnection(e.LocalEntity, payload.Entity); err == nil { + filter := model.ElectricalConnectionParameterDescriptionDataType{ + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + } + data, err := ec.GetParameterDescriptionsForFilter(filter) + if err != nil || len(data) == 0 || data[0].ParameterId == nil { + return + } + + filter = model.ElectricalConnectionParameterDescriptionDataType{ + ParameterId: data[0].ParameterId, + } + values, err := ec.GetParameterDescriptionsForFilter(filter) + if err != nil || values == nil { + return + } + + // Scenario 6 + filter1 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: values[0].ElectricalConnectionId, + ParameterId: values[0].ParameterId, + } + if ec.CheckEventPayloadDataForFilter(payload.Data, filter1) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentLimits) + } + } +} diff --git a/usecases/cem/opev/events_test.go b/usecases/cem/opev/events_test.go new file mode 100644 index 00000000..44a8ca5d --- /dev/null +++ b/usecases/cem/opev/events_test.go @@ -0,0 +1,177 @@ +package opev + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *OPEVSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + 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 = util.Ptr(model.ElectricalConnectionPermittedValueSetListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *OPEVSuite) Test_Failures() { + s.sut.evConnected(s.mockRemoteEntity) + + s.sut.evLoadControlLimitDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *OPEVSuite) Test_evElectricalPermittedValuesUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{ + *model.NewScaledNumberType(0.1), + }, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(1400), + Max: model.NewScaledNumberType(11000), + }, + }, + }, + }, + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + }, + }, + } + + payload.Data = data + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *OPEVSuite) Test_evLoadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + ScopeType: util.Ptr(model.ScopeTypeTypeOverloadProtection), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/opev/public.go b/usecases/cem/opev/public.go new file mode 100644 index 00000000..97f0f25d --- /dev/null +++ b/usecases/cem/opev/public.go @@ -0,0 +1,80 @@ +package opev + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the min, max, default limits for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemOPEV) CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, nil, nil, api.ErrNoCompatibleEntity + } + + ec, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return nil, nil, nil, err + } + + return ec.GetPhaseCurrentLimits() +} + +// return the current loadcontrol obligation limits +// +// parameters: +// - entity: the entity of the EV +// +// return values: +// - limits: per phase data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *CemOPEV) LoadControlLimits(entity spineapi.EntityRemoteInterface) ( + limits []ucapi.LoadLimitsPhase, resultErr error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + ScopeType: util.Ptr(model.ScopeTypeTypeOverloadProtection), + } + return internal.LoadControlLimits(e.LocalEntity, entity, filter) +} + +// send new LoadControlLimits to the remote EV +// +// parameters: +// - limits: a set of limits containing phase specific limit data +// +// Sets a maximum A limit for each phase that the EV may not exceed. +// Mainly used for implementing overload protection of the site or limiting the +// maximum charge power of EVs when the EV and EVSE communicate via IEC61851 +// and with ISO15118 if the EV does not support the Optimization of Self Consumption +// usecase. +// +// note: +// For obligations to work for optimizing solar excess power, the EV needs to +// have an energy demand. Recommendations work even if the EV does not have an active +// energy demand, given it communicated with the EVSE via ISO15118 and supports the usecase. +// In ISO15118-2 the usecase is only supported via VAS extensions which are vendor specific +// and needs to have specific EVSE support for the specific EV brand. +// In ISO15118-20 this is a standard feature which does not need special support on the EVSE. +func (e *CemOPEV) WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []ucapi.LoadLimitsPhase) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + return internal.WriteLoadControlLimits(e.LocalEntity, entity, model.LoadControlCategoryTypeObligation, limits) +} diff --git a/usecases/cem/opev/public_test.go b/usecases/cem/opev/public_test.go new file mode 100644 index 00000000..e5063816 --- /dev/null +++ b/usecases/cem/opev/public_test.go @@ -0,0 +1,28 @@ +package opev + +import ( + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/stretchr/testify/assert" +) + +func (s *OPEVSuite) Test_Public() { + // The actual tests of the functionality is located in the util package + + _, _, _, err := s.sut.CurrentLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, _, _, err = s.sut.CurrentLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.mockRemoteEntity, []ucapi.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.evEntity, []ucapi.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) +} diff --git a/usecases/cem/opev/testhelper_test.go b/usecases/cem/opev/testhelper_test.go new file mode 100644 index 00000000..091826f2 --- /dev/null +++ b/usecases/cem/opev/testhelper_test.go @@ -0,0 +1,189 @@ +package opev + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestOPEVSuite(t *testing.T) { + suite.Run(t, new(OPEVSuite)) +} + +type OPEVSuite struct { + suite.Suite + + sut *CemOPEV + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *OPEVSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *OPEVSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemOPEV(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeLoadControl, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + []model.FunctionType{ + model.FunctionTypeDeviceDiagnosisStateData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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 +} diff --git a/usecases/cem/opev/types.go b/usecases/cem/opev/types.go new file mode 100644 index 00000000..005c6b00 --- /dev/null +++ b/usecases/cem/opev/types.go @@ -0,0 +1,15 @@ +package opev + +import "github.com/enbility/eebus-go/api" + +const ( + // EV current limits + // + // Use `CurrentLimits` to get the current data + DataUpdateCurrentLimits api.EventType = "cem-opev-DataUpdateCurrentLimits" + + // EV load control obligation limit data updated + // + // Use `LoadControlLimits` to get the current data + DataUpdateLimit api.EventType = "cem-opev-DataUpdateLimit" +) diff --git a/usecases/cem/opev/usecase.go b/usecases/cem/opev/usecase.go new file mode 100644 index 00000000..94a5410e --- /dev/null +++ b/usecases/cem/opev/usecase.go @@ -0,0 +1,97 @@ +package opev + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CemOPEV struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemOPEVInterface = (*CemOPEV)(nil) + +func NewCemOPEV(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemOPEV { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEV, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeOverloadProtectionByEVChargingCurrentCurtailment, + "1.0.1", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3}, + eventCB, + validEntityTypes, + ) + + uc := &CemOPEV{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemOPEV) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisStateData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemOPEV) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeEV, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1, 2, 3}, + []model.FeatureTypeType{model.FeatureTypeTypeLoadControl}, + ) { + return false, nil + } + + // check for required features + evLoadControl, err := client.NewLoadControl(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if loadcontrol limit descriptions contains a obligation category + filter := model.LoadControlLimitDescriptionDataType{ + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + } + if data, err := evLoadControl.GetLimitDescriptionsForFilter(filter); err != nil || len(data) == 0 { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/cem/opev/usecase_test.go b/usecases/cem/opev/usecase_test.go new file mode 100644 index 00000000..76dbb49c --- /dev/null +++ b/usecases/cem/opev/usecase_test.go @@ -0,0 +1,62 @@ +package opev + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *OPEVSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *OPEVSuite) 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeOverloadProtectionByEVChargingCurrentCurtailment), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + } + + 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.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/oscev/events.go b/usecases/cem/oscev/events.go new file mode 100644 index 00000000..7c35a82c --- /dev/null +++ b/usecases/cem/oscev/events.go @@ -0,0 +1,78 @@ +package oscev + +import ( + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemOSCEV) HandleEvent(payload spineapi.EventPayload) { + // most of the events are identical to OPEV, and OPEV is required to be used, + // we don't handle the same events in here + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.ElectricalConnectionPermittedValueSetListDataType: + e.evElectricalPermittedValuesUpdate(payload) + case *model.LoadControlLimitListDataType: + e.evLoadControlLimitDataUpdate(payload) + } +} + +// the load control limit data of an EV was updated +func (e *CemOSCEV) evLoadControlLimitDataUpdate(payload spineapi.EventPayload) { + lc, err := client.NewLoadControl(e.LocalEntity, payload.Entity) + if err != nil { + return + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + ScopeType: util.Ptr(model.ScopeTypeTypeSelfConsumption), + } + + if lc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } +} + +// the electrical connection permitted value sets data of an EV was updated +func (e *CemOSCEV) evElectricalPermittedValuesUpdate(payload spineapi.EventPayload) { + if ec, err := client.NewElectricalConnection(e.LocalEntity, payload.Entity); err == nil { + filter := model.ElectricalConnectionParameterDescriptionDataType{ + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + } + data, err := ec.GetParameterDescriptionsForFilter(filter) + if err != nil || len(data) == 0 || data[0].ParameterId == nil { + return + } + + filter = model.ElectricalConnectionParameterDescriptionDataType{ + ParameterId: data[0].ParameterId, + } + values, err := ec.GetParameterDescriptionsForFilter(filter) + if err != nil || values == nil { + return + } + + // Scenario 6 + filter1 := model.ElectricalConnectionParameterDescriptionDataType{ + ElectricalConnectionId: values[0].ElectricalConnectionId, + ParameterId: values[0].ParameterId, + } + if ec.CheckEventPayloadDataForFilter(payload.Data, filter1) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentLimits) + } + } +} diff --git a/usecases/cem/oscev/events_test.go b/usecases/cem/oscev/events_test.go new file mode 100644 index 00000000..b7b705d9 --- /dev/null +++ b/usecases/cem/oscev/events_test.go @@ -0,0 +1,168 @@ +package oscev + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *OSCEVSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.evEntity + 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 = util.Ptr(model.ElectricalConnectionPermittedValueSetListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *OSCEVSuite) Test_evElectricalPermittedValuesUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{ + *model.NewScaledNumberType(0.1), + }, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(1400), + Max: model.NewScaledNumberType(11000), + }, + }, + }, + }, + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + }, + }, + } + + payload.Data = data + + s.sut.evElectricalPermittedValuesUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *OSCEVSuite) Test_evLoadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.mockRemoteEntity, + } + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Entity = s.evEntity + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + ScopeType: util.Ptr(model.ScopeTypeTypeSelfConsumption), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.evLoadControlLimitDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/oscev/public.go b/usecases/cem/oscev/public.go new file mode 100644 index 00000000..3345da4e --- /dev/null +++ b/usecases/cem/oscev/public.go @@ -0,0 +1,73 @@ +package oscev + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the min, max, default limits for each phase of the connected EV +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemOSCEV) CurrentLimits(entity spineapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, nil, nil, api.ErrNoCompatibleEntity + } + + ec, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return nil, nil, nil, err + } + + return ec.GetPhaseCurrentLimits() +} + +// return the current loadcontrol recommendation limits +// +// parameters: +// - entity: the entity of the EV +// +// return values: +// - limits: per phase data +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *CemOSCEV) LoadControlLimits(entity spineapi.EntityRemoteInterface) ( + limits []ucapi.LoadLimitsPhase, resultErr error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeMaxValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + ScopeType: util.Ptr(model.ScopeTypeTypeOverloadProtection), + } + return internal.LoadControlLimits(e.LocalEntity, entity, filter) +} + +// send new LoadControlLimits to the remote EV +// +// parameters: +// - limits: a set of limits containing phase specific limit data +// +// recommendations: +// Sets a recommended charge power in A for each phase. This is mainly +// used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. +// The EV either needs to support the Optimization of Self Consumption usecase or +// the EVSE needs to be able map the recommendations into oligation limits which then +// works for all EVs communication either via IEC61851 or ISO15118. +func (e *CemOSCEV) WriteLoadControlLimits(entity spineapi.EntityRemoteInterface, limits []ucapi.LoadLimitsPhase) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + return internal.WriteLoadControlLimits(e.LocalEntity, entity, model.LoadControlCategoryTypeRecommendation, limits) +} diff --git a/usecases/cem/oscev/public_test.go b/usecases/cem/oscev/public_test.go new file mode 100644 index 00000000..0068af40 --- /dev/null +++ b/usecases/cem/oscev/public_test.go @@ -0,0 +1,28 @@ +package oscev + +import ( + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/stretchr/testify/assert" +) + +func (s *OSCEVSuite) Test_Public() { + // The actual tests of the functionality is located in the util package + + _, _, _, err := s.sut.CurrentLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, _, _, err = s.sut.CurrentLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.LoadControlLimits(s.evEntity) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.mockRemoteEntity, []ucapi.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteLoadControlLimits(s.evEntity, []ucapi.LoadLimitsPhase{}) + assert.NotNil(s.T(), err) +} diff --git a/usecases/cem/oscev/testhelper_test.go b/usecases/cem/oscev/testhelper_test.go new file mode 100644 index 00000000..5f88f75b --- /dev/null +++ b/usecases/cem/oscev/testhelper_test.go @@ -0,0 +1,198 @@ +package oscev + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestOSCEVSuite(t *testing.T) { + suite.Run(t, new(OSCEVSuite)) +} + +type OSCEVSuite struct { + suite.Suite + + sut *CemOSCEV + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evEntity spineapi.EntityRemoteInterface + eventCalled bool +} + +func (s *OSCEVSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *OSCEVSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemOSCEV(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeDeviceDiagnosis, + 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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + 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 +} diff --git a/usecases/cem/oscev/types.go b/usecases/cem/oscev/types.go new file mode 100644 index 00000000..a1273646 --- /dev/null +++ b/usecases/cem/oscev/types.go @@ -0,0 +1,17 @@ +package oscev + +import "github.com/enbility/eebus-go/api" + +const ( + // EV current limits + // + // Use `CurrentLimits` to get the current data + DataUpdateCurrentLimits api.EventType = "cem-oscev-DataUpdateCurrentLimits" + + // EV load control recommendation limit data updated + // + // Use `LoadControlLimits` to get the current data + // + // Use Case OSCEV, Scenario 1 + DataUpdateLimit api.EventType = "cem-oscev-DataUpdateLimit" +) diff --git a/usecases/cem/oscev/usecase.go b/usecases/cem/oscev/usecase.go new file mode 100644 index 00000000..ecc94641 --- /dev/null +++ b/usecases/cem/oscev/usecase.go @@ -0,0 +1,103 @@ +package oscev + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CemOSCEV struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemOSCEVInterface = (*CemOSCEV)(nil) + +func NewCemOSCEV(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemOSCEV { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + model.EntityTypeTypeElectricalImmersionHeater, + model.EntityTypeTypeEV, + model.EntityTypeTypeHeatPumpAppliance, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging, + "1.0.1", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3}, + eventCB, + validEntityTypes, + ) + + uc := &CemOSCEV{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemOSCEV) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisStateData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemOSCEV) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if entity == nil || entity.EntityType() != model.EntityTypeTypeEV { + 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.UseCaseActorTypeEV, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1, 2, 3}, + []model.FeatureTypeType{model.FeatureTypeTypeLoadControl}, + ) { + return false, nil + } + + // check for required features + evLoadControl, err := client.NewLoadControl(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if loadcontrol limit descriptions contains a recommendation category + filter := model.LoadControlLimitDescriptionDataType{ + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + } + if data, err := evLoadControl.GetLimitDescriptionsForFilter(filter); err != nil || len(data) == 0 { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/cem/oscev/usecase_test.go b/usecases/cem/oscev/usecase_test.go new file mode 100644 index 00000000..90ee0840 --- /dev/null +++ b/usecases/cem/oscev/usecase_test.go @@ -0,0 +1,62 @@ +package oscev + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *OSCEVSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *OSCEVSuite) 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.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeEV), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeOptimizationOfSelfConsumptionDuringEVCharging), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + } + + 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.evEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.evEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.evEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/vabd/events.go b/usecases/cem/vabd/events.go new file mode 100644 index 00000000..de167b45 --- /dev/null +++ b/usecases/cem/vabd/events.go @@ -0,0 +1,110 @@ +package vabd + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemVABD) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.inverterConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.MeasurementDescriptionListDataType: + e.inverterMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.inverterMeasurementDataUpdate(payload) + } +} + +// process required steps when a grid device is connected +func (e *CemVABD) inverterConnected(entity spineapi.EntityRemoteInterface) { + if electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the measurement descriptiondata of an SMGW was updated +func (e *CemVABD) inverterMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestData(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *CemVABD) inverterMeasurementDataUpdate(payload spineapi.EventPayload) { + if measurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + // Scenario 1 + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + // Scenario 2 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeCharge) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyCharged) + } + + // Scenario 3 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeDischarge) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyDischarged) + } + + // Scenario 4 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeStateOfCharge) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateStateOfCharge) + } + } +} diff --git a/usecases/cem/vabd/events_test.go b/usecases/cem/vabd/events_test.go new file mode 100644 index 00000000..027eb6e8 --- /dev/null +++ b/usecases/cem/vabd/events_test.go @@ -0,0 +1,110 @@ +package vabd + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *VABDSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.batteryEntity + 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 = util.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *VABDSuite) Test_Failures() { + s.sut.inverterConnected(s.mockRemoteEntity) + + s.sut.inverterMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *VABDSuite) Test_inverterMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.batteryEntity, + } + s.sut.inverterMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + ScopeType: util.Ptr(model.ScopeTypeTypeDischarge), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(3)), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(3)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.inverterMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/vabd/public.go b/usecases/cem/vabd/public.go new file mode 100644 index 00000000..4f9bbe20 --- /dev/null +++ b/usecases/cem/vabd/public.go @@ -0,0 +1,106 @@ +package vabd + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the current battery (dis-)charge power (W) +// +// - positive values charge power +// - negative values discharge power +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVABD) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + return e.getValuesFoFilter(entity, filter) +} + +// return the total charge energy (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVABD) EnergyCharged(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + } + return e.getValuesFoFilter(entity, filter) +} + +// return the total discharge energy (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVABD) EnergyDischarged(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeDischarge), + } + return e.getValuesFoFilter(entity, filter) +} + +// return the current state of charge in % +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVABD) StateOfCharge(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + } + return e.getValuesFoFilter(entity, filter) +} + +// helper + +func (e *CemVABD) getValuesFoFilter( + entity spineapi.EntityRemoteInterface, + filter model.MeasurementDescriptionDataType, +) (float64, error) { + if entity == nil { + return 0, api.ErrDeviceDisconnected + } + + measurementF, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, api.ErrFunctionNotSupported + } + + result, err := measurementF.GetDataForFilter(filter) + if err != nil || len(result) == 0 || result[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + return result[0].Value.GetValue(), nil +} diff --git a/usecases/cem/vabd/public_test.go b/usecases/cem/vabd/public_test.go new file mode 100644 index 00000000..33068d36 --- /dev/null +++ b/usecases/cem/vabd/public_test.go @@ -0,0 +1,187 @@ +package vabd + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *VABDSuite) Test_CurrentChargePower() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *VABDSuite) Test_TotalChargeEnergy() { + data, err := s.sut.EnergyCharged(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyCharged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeCharge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyCharged(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *VABDSuite) Test_TotalDischargeEnergy() { + data, err := s.sut.EnergyDischarged(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyDischarged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeDischarge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyDischarged(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyDischarged(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *VABDSuite) Test_CurrentStateOfCharge() { + data, err := s.sut.StateOfCharge(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.StateOfCharge(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePercentage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.StateOfCharge(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} diff --git a/usecases/cem/vabd/testhelper_test.go b/usecases/cem/vabd/testhelper_test.go new file mode 100644 index 00000000..2d8120fc --- /dev/null +++ b/usecases/cem/vabd/testhelper_test.go @@ -0,0 +1,173 @@ +package vabd + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestVABDSuite(t *testing.T) { + suite.Run(t, new(VABDSuite)) +} + +type VABDSuite struct { + suite.Suite + + sut *CemVABD + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + batteryEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *VABDSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *VABDSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemVABD(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.batteryEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeElectricityStorageSystem), + }, + }, + }, + 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/usecases/cem/vabd/types.go b/usecases/cem/vabd/types.go new file mode 100644 index 00000000..398ce13e --- /dev/null +++ b/usecases/cem/vabd/types.go @@ -0,0 +1,33 @@ +package vabd + +import "github.com/enbility/eebus-go/api" + +const ( + // Battery System (dis)charge power data updated + // + // Use `Power` to get the current data + // + // Use Case VABD, Scenario 1 + DataUpdatePower api.EventType = "cem-vabd-DataUpdatePower" + + // Battery System cumulated charge energy data updated + // + // Use `EnergyCharged` to get the current data + // + // Use Case VABD, Scenario 2 + DataUpdateEnergyCharged api.EventType = "cem-vabd-DataUpdateEnergyCharged" + + // Battery System cumulated discharge energy data updated + // + // Use `EnergyDischarged` to get the current data + // + // Use Case VABD, Scenario 3 + DataUpdateEnergyDischarged api.EventType = "cem-vabd-DataUpdateEnergyDischarged" + + // Battery System state of charge data updated + // + // Use `StateOfCharge` to get the current data + // + // Use Case VABD, Scenario 4 + DataUpdateStateOfCharge api.EventType = "cem-vabd-DataUpdateStateOfCharge" +) diff --git a/usecases/cem/vabd/usecase.go b/usecases/cem/vabd/usecase.go new file mode 100644 index 00000000..7a3e2764 --- /dev/null +++ b/usecases/cem/vabd/usecase.go @@ -0,0 +1,114 @@ +package vabd + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CemVABD struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemVABDInterface = (*CemVABD)(nil) + +func NewCemVABD(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemVABD { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeElectricityStorageSystem, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeVisualizationOfAggregatedBatteryData, + "1.0.1", + "RC1", + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + eventCB, + validEntityTypes, + ) + + uc := &CemVABD{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemVABD) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemVABD) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypePVSystem, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check for required features + electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if electrical connection descriptions and parameter descriptions are available name + if _, err = electricalConnection.GetDescriptionsForFilter(model.ElectricalConnectionDescriptionDataType{}); err != nil { + return false, err + } + if _, err = electricalConnection.GetParameterDescriptionsForFilter(model.ElectricalConnectionParameterDescriptionDataType{}); err != nil { + return false, err + } + + // check for required features + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if measurement descriptions contains a required scope + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if data, err := measurement.GetDescriptionsForFilter(filter); data == nil || err != nil { + return false, api.ErrFunctionNotSupported + } + filter.ScopeType = util.Ptr(model.ScopeTypeTypeStateOfCharge) + if data, err := measurement.GetDescriptionsForFilter(filter); data == nil || err != nil { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/cem/vabd/usecase_test.go b/usecases/cem/vabd/usecase_test.go new file mode 100644 index 00000000..f9278dc3 --- /dev/null +++ b/usecases/cem/vabd/usecase_test.go @@ -0,0 +1,97 @@ +package vabd + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *VABDSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *VABDSuite) 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.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypePVSystem), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeVisualizationOfAggregatedBatteryData), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 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.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeStateOfCharge), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.batteryEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.batteryEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cem/vapd/events.go b/usecases/cem/vapd/events.go new file mode 100644 index 00000000..2b2a5377 --- /dev/null +++ b/usecases/cem/vapd/events.go @@ -0,0 +1,137 @@ +package vapd + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CemVAPD) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.inverterConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.inverterConfigurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.inverterConfigurationDataUpdate(payload) + case *model.MeasurementDescriptionListDataType: + e.inverterMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.inverterMeasurementDataUpdate(payload) + } +} + +// process required steps when a grid device is connected +func (e *CemVAPD) inverterConnected(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity); err == nil { + if _, err := deviceConfiguration.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get configuration data + if _, err := deviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the configuration key description data of an SMGW was updated +func (e *CemVAPD) inverterConfigurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, 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 measurement data of an SMGW was updated +func (e *CemVAPD) inverterConfigurationDataUpdate(payload spineapi.EventPayload) { + // Scenario 1 + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, payload.Entity); err == nil { + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + } + if deviceConfiguration.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerNominalPeak) + } + } +} + +// the measurement descriptiondata of an SMGW was updated +func (e *CemVAPD) inverterMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestData(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *CemVAPD) inverterMeasurementDataUpdate(payload spineapi.EventPayload) { + if measurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + // Scenario 2 + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + // Scenario 3 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACYieldTotal) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePVYieldTotal) + } + } +} diff --git a/usecases/cem/vapd/events_test.go b/usecases/cem/vapd/events_test.go new file mode 100644 index 00000000..887de937 --- /dev/null +++ b/usecases/cem/vapd/events_test.go @@ -0,0 +1,142 @@ +package vapd + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *VAPDSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.pvEntity + 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 = util.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *VAPDSuite) Test_Failures() { + s.sut.inverterConnected(s.mockRemoteEntity) + + s.sut.inverterConfigurationDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.inverterMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *VAPDSuite) Test_inverterConfigurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.pvEntity, + } + s.sut.inverterConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + + payload.Data = data + + s.sut.inverterConfigurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *VAPDSuite) Test_inverterMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.pvEntity, + } + s.sut.inverterMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeACYieldTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.inverterMeasurementDescriptionDataUpdate(payload.Entity) + assert.False(s.T(), s.eventCalled) + + s.sut.inverterMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.inverterMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cem/vapd/public.go b/usecases/cem/vapd/public.go new file mode 100644 index 00000000..461df9bf --- /dev/null +++ b/usecases/cem/vapd/public.go @@ -0,0 +1,102 @@ +package vapd + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the current photovoltaic production power (W) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVAPD) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + return e.getValuesFoFilter(entity, filter) +} + +// return the nominal photovoltaic peak power (W) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVAPD) PowerNominalPeak(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil { + return 0, api.ErrFunctionNotSupported + } + + keyName := model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyName, + } + if _, err := deviceConfiguration.GetKeyValueDescriptionsForFilter(filter); err != nil { + return 0, err + } + + filter = model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyName, + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil || data.Value == nil || data.Value.ScaledNumber == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.ScaledNumber.GetValue(), nil +} + +// return the total photovoltaic yield (Wh) +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func (e *CemVAPD) PVYieldTotal(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACYieldTotal), + } + return e.getValuesFoFilter(entity, filter) +} + +// helper + +func (e *CemVAPD) getValuesFoFilter( + entity spineapi.EntityRemoteInterface, + filter model.MeasurementDescriptionDataType, +) (float64, error) { + if entity == nil { + return 0, api.ErrDeviceDisconnected + } + + measurementF, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, api.ErrFunctionNotSupported + } + + result, err := measurementF.GetDataForFilter(filter) + if err != nil || len(result) == 0 || result[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + return result[0].Value.GetValue(), nil +} diff --git a/usecases/cem/vapd/public_test.go b/usecases/cem/vapd/public_test.go new file mode 100644 index 00000000..8a6d8864 --- /dev/null +++ b/usecases/cem/vapd/public_test.go @@ -0,0 +1,138 @@ +package vapd + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *VAPDSuite) Test_CurrentProductionPower() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *VAPDSuite) Test_NominalPeakPower() { + data, err := s.sut.PowerNominalPeak(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerNominalPeak(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + confData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + confFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := confFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, confData, nil, nil) + assert.Nil(s.T(), fErr) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + fErr = confFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerNominalPeak(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *VAPDSuite) Test_TotalPVYield() { + data, err := s.sut.PVYieldTotal(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PVYieldTotal(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACYieldTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PVYieldTotal(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PVYieldTotal(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} diff --git a/usecases/cem/vapd/testhelper_test.go b/usecases/cem/vapd/testhelper_test.go new file mode 100644 index 00000000..4ddd31f1 --- /dev/null +++ b/usecases/cem/vapd/testhelper_test.go @@ -0,0 +1,179 @@ +package vapd + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestVAPDSuite(t *testing.T) { + suite.Run(t, new(VAPDSuite)) +} + +type VAPDSuite struct { + suite.Suite + + sut *CemVAPD + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + pvEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *VAPDSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *VAPDSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCemVAPD(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.pvEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypePVSystem), + }, + }, + }, + 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/usecases/cem/vapd/types.go b/usecases/cem/vapd/types.go new file mode 100644 index 00000000..b853ea45 --- /dev/null +++ b/usecases/cem/vapd/types.go @@ -0,0 +1,26 @@ +package vapd + +import "github.com/enbility/eebus-go/api" + +const ( + // PV System total power data updated + // + // Use `Power` to get the current data + // + // Use Case VAPD, Scenario 1 + DataUpdatePower api.EventType = "cem-vapd-DataUpdatePower" + + // PV System nominal peak power data updated + // + // Use `PowerNominalPeak` to get the current data + // + // Use Case VAPD, Scenario 2 + DataUpdatePowerNominalPeak api.EventType = "cem-vapd-DataUpdatePowerNominalPeak" + + // PV System total yield data updated + // + // Use `PVYieldTotal` to get the current data + // + // Use Case VAPD, Scenario 3 + DataUpdatePVYieldTotal api.EventType = "cem-vapd-DataUpdatePVYieldTotal" +) diff --git a/usecases/cem/vapd/usecase.go b/usecases/cem/vapd/usecase.go new file mode 100644 index 00000000..a3c73c8a --- /dev/null +++ b/usecases/cem/vapd/usecase.go @@ -0,0 +1,128 @@ +package vapd + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CemVAPD struct { + *usecase.UseCaseBase +} + +var _ ucapi.CemVAPDInterface = (*CemVAPD)(nil) + +func NewCemVAPD(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CemVAPD { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypePVSystem, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeVisualizationOfAggregatedPhotovoltaicData, + "1.0.1", + "RC1", + []model.UseCaseScenarioSupportType{1, 2, 3}, + eventCB, + validEntityTypes, + ) + + uc := &CemVAPD{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CemVAPD) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CemVAPD) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypePVSystem, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1, 2, 3}, + []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check for required features + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if device configuration descriptions contains a required key name + filter1 := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + } + if _, err = deviceConfiguration.GetKeyValueDescriptionsForFilter(filter1); err != nil { + return false, err + } + + electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if electrical connection descriptions and parameter descriptions are available name + if _, err = electricalConnection.GetDescriptionsForFilter(model.ElectricalConnectionDescriptionDataType{}); err != nil { + return false, err + } + if _, err = electricalConnection.GetParameterDescriptionsForFilter(model.ElectricalConnectionParameterDescriptionDataType{}); err != nil { + return false, err + } + + // check for required features + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + // check if measurement descriptions contains a required scope + filter2 := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if data, err := measurement.GetDescriptionsForFilter(filter2); data == nil || err != nil { + return false, api.ErrFunctionNotSupported + } + filter2.ScopeType = util.Ptr(model.ScopeTypeTypeACYieldTotal) + if data, err := measurement.GetDescriptionsForFilter(filter2); data == nil || err != nil { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/cem/vapd/usecase_test.go b/usecases/cem/vapd/usecase_test.go new file mode 100644 index 00000000..90cbb456 --- /dev/null +++ b/usecases/cem/vapd/usecase_test.go @@ -0,0 +1,114 @@ +package vapd + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *VAPDSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *VAPDSuite) 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.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypePVSystem), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeVisualizationOfAggregatedPhotovoltaicData), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1, 2, 3}, + }, + }, + }, + }, + } + + 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.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + confData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePeakPowerOfPVSystem), + }, + }, + } + + confFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr = confFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, confData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeACYieldTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.pvEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.pvEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/cs/lpc/events.go b/usecases/cs/lpc/events.go new file mode 100644 index 00000000..589825dc --- /dev/null +++ b/usecases/cs/lpc/events.go @@ -0,0 +1,171 @@ +package lpc + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/features/server" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CsLPC) HandleEvent(payload spineapi.EventPayload) { + if internal.IsDeviceConnected(payload) { + e.deviceConnected(payload) + return + } + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + // 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 internal.IsHeartbeat(payload) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateHeartbeat) + return + } + + if 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 := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeLoadControlLimitListData || + payload.LocalFeature != serverF { + return + } + + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueListDataType: + serverF := e.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 *CsLPC) 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 !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceDiagnosis(e.LocalEntity, deviceDiagEntites[0]); err == nil { + e.heartbeatDiag = localDeviceDiag + 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 *CsLPC) subscribeHeartbeatWorkaround(payload spineapi.EventPayload) { + // is the workaround is needed? + if e.heartbeatKeoWorkaround { + if localDeviceDiag, err := client.NewDeviceDiagnosis(e.LocalEntity, payload.Entity); err == nil { + e.heartbeatDiag = localDeviceDiag + 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 *CsLPC) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if lc, err := server.NewLoadControl(e.LocalEntity); err == nil { + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + if lc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } + } +} + +// the configuration key data of an SMGW was updated +func (e *CsLPC) configurationDataUpdate(payload spineapi.EventPayload) { + if dc, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeConsumptionActivePowerLimit) + } + filter = model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } + } +} diff --git a/usecases/cs/lpc/events_test.go b/usecases/cs/lpc/events_test.go new file mode 100644 index 00000000..b169a8ba --- /dev/null +++ b/usecases/cs/lpc/events_test.go @@ -0,0 +1,332 @@ +package lpc + +import ( + "fmt" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPCSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + EventType: spineapi.EventTypeSubscriptionChange, + } + 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 = util.Ptr(model.CmdClassifierTypeWrite) + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeLoadControlLimitListData + payload.Data = util.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) + + payload.Function = model.FunctionTypeDeviceConfigurationKeyValueListData + payload.Data = util.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) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeDeviceDiagnosisHeartbeatData + payload.LocalFeature = s.deviceDiagnosisFeature + payload.CmdClassifier = util.Ptr(model.CmdClassifierTypeNotify) + payload.Data = util.Ptr(model.DeviceDiagnosisHeartbeatDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *LPCSuite) 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 *LPCSuite) 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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{model.AddressEntityType(i)}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{2}, + }, + EntityType: util.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{3}, + }, + EntityType: util.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{4}, + }, + EntityType: util.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) +} + +func (s *LPCSuite) Test_loadControlLimitDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + lFeature.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, descData) + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *LPCSuite) Test_configurationDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + LocalFeature: lFeature, + } + + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + lFeature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + s.eventCalled = false + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.eventCalled = false + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + }, + } + + payload.Data = data + + s.eventCalled = false + s.sut.configurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go new file mode 100644 index 00000000..0034986c --- /dev/null +++ b/usecases/cs/lpc/public.go @@ -0,0 +1,332 @@ +package lpc + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/server" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// 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 +func (e *CsLPC) ConsumptionLimit() (limit ucapi.LoadLimit, resultErr error) { + limit = ucapi.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + Duration: 0, + } + resultErr = api.ErrDataNotAvailable + + lc, limidId, err := e.loadControlServerAndLimitId() + if err != nil { + return limit, err + } + + value, err := lc.GetLimitDataForId(limidId) + if err != nil || value == nil || 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 loadcontrol limit data +func (e *CsLPC) SetConsumptionLimit(limit ucapi.LoadLimit) (resultErr error) { + loadControlf, limidId, err := e.loadControlServerAndLimitId() + if err != nil { + return err + } + + limitData := model.LoadControlLimitDataType{ + LimitId: util.Ptr(limidId), + IsLimitChangeable: util.Ptr(limit.IsChangeable), + IsLimitActive: util.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + limitData.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + + deleteTimePeriod := &model.LoadControlLimitDataElementsType{ + TimePeriod: util.Ptr(model.TimePeriodElementsType{}), + } + + return loadControlf.UpdateLimitDataForId(limitData, deleteTimePeriod, limidId) +} + +// return the currently pending incoming consumption write limits +func (e *CsLPC) PendingConsumptionLimits() map[model.MsgCounterType]ucapi.LoadLimit { + result := make(map[model.MsgCounterType]ucapi.LoadLimit) + + _, limitId, err := e.loadControlServerAndLimitId() + if err != nil { + return result + } + + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + for key, msg := range e.pendingLimits { + data := msg.Cmd.LoadControlLimitListData + + // elements are only added to the map if all required fields exist + // therefor not check for these are needed here + + // find the item which contains the limit for this usecase + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + limit := ucapi.LoadLimit{} + + if item.TimePeriod != nil { + if duration, err := item.TimePeriod.GetDuration(); err == nil { + limit.Duration = duration + } + } + + if item.IsLimitActive != nil { + limit.IsActive = *item.IsLimitActive + } + + if item.Value != nil { + limit.Value = item.Value.GetValue() + } + + result[key] = limit + } + } + + return result +} + +// accept or deny an incoming consumption write limit +// +// use PendingConsumptionLimits to get the list of currently pending requests +func (e *CsLPC) ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + msg, ok := e.pendingLimits[msgCounter] + if !ok { + return + } + + f := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + result := model.ErrorType{ + ErrorNumber: model.ErrorNumberType(0), + } + if !approve { + result.ErrorNumber = model.ErrorNumberType(7) + result.Description = util.Ptr(model.DescriptionType(reason)) + } + f.ApproveOrDenyWrite(msg, result) +} + +// Scenario 2 + +// return Failsafe limit for the consumed active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *CsLPC) FailsafeConsumptionActivePowerLimit() (limit float64, isChangeable bool, resultErr error) { + limit = 0 + isChangeable = false + resultErr = api.ErrDataNotAvailable + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + } + keyData, err := dc.GetKeyValueDataForFilter(filter) + if err != nil || keyData == nil || 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 consumed active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *CsLPC) SetFailsafeConsumptionActivePowerLimit(value float64, changeable bool) error { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + keyValue := model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + } + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return err + } + + data := model.DeviceConfigurationKeyValueDataType{ + Value: &keyValue, + IsValueChangeable: &changeable, + } + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(keyName), + } + return dc.UpdateKeyValueDataForFilter(data, nil, filter) +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *CsLPC) FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) { + duration = 0 + isChangeable = false + resultErr = api.ErrDataNotAvailable + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + } + keyData, err := dc.GetKeyValueDataForFilter(filter) + if err != nil || keyData == nil || 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 *CsLPC) 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), + } + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return err + } + + data := model.DeviceConfigurationKeyValueDataType{ + Value: &keyValue, + IsValueChangeable: &changeable, + } + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(keyName), + } + return dc.UpdateKeyValueDataForFilter(data, nil, filter) +} + +// Scenario 3 + +func (e *CsLPC) IsHeartbeatWithinDuration() bool { + if e.heartbeatDiag == nil { + return false + } + + return e.heartbeatDiag.IsHeartbeatWithinDuration(2 * time.Minute) +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// allowed to consume due to the customer's contract. +func (e *CsLPC) ContractualConsumptionNominalMax() (value float64, resultErr error) { + value = 0 + resultErr = api.ErrDataNotAvailable + + ec, err := server.NewElectricalConnection(e.LocalEntity) + if err != nil { + resultErr = err + return + } + + filter := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax), + } + charData, err := ec.GetCharacteristicsForFilter(filter) + if err != nil || len(charData) == 0 || + charData[0].CharacteristicId == nil || + charData[0].Value == nil { + return + } + + return charData[0].Value.GetValue(), nil +} + +// set nominal maximum active (real) power the Controllable System is +// allowed to consume due to the customer's contract. +func (e *CsLPC) SetContractualConsumptionNominalMax(value float64) error { + ec, err := server.NewElectricalConnection(e.LocalEntity) + if err != nil { + return err + } + + electricalConnectionid := util.Ptr(model.ElectricalConnectionIdType(0)) + parameterId := util.Ptr(model.ElectricalConnectionParameterIdType(0)) + charList, err := ec.GetCharacteristicsForFilter(model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: electricalConnectionid, + ParameterId: parameterId, + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax), + }) + if err != nil || len(charList) == 0 { + return api.ErrDataNotAvailable + } + + data := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: electricalConnectionid, + ParameterId: parameterId, + CharacteristicId: charList[0].CharacteristicId, + Value: model.NewScaledNumberType(value), + } + return ec.UpdateCharacteristic(data, nil) +} diff --git a/usecases/cs/lpc/public_test.go b/usecases/cs/lpc/public_test.go new file mode 100644 index 00000000..d5a7090f --- /dev/null +++ b/usecases/cs/lpc/public_test.go @@ -0,0 +1,154 @@ +package lpc + +import ( + "time" + + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPCSuite) Test_ConsumptionLimit() { + limit, err := s.sut.ConsumptionLimit() + assert.Equal(s.T(), 0.0, limit.Value) + assert.NotNil(s.T(), err) + + newLimit := ucapi.LoadLimit{ + Duration: time.Duration(time.Hour * 2), + IsActive: true, + IsChangeable: true, + Value: 16, + } + err = s.sut.SetConsumptionLimit(newLimit) + assert.Nil(s.T(), err) + + limit, err = s.sut.ConsumptionLimit() + assert.Equal(s.T(), 16.0, limit.Value) + assert.Nil(s.T(), err) +} + +func (s *LPCSuite) Test_PendingConsumptionLimits() { + data := s.sut.PendingConsumptionLimits() + assert.Equal(s.T(), 0, len(data)) + + msgCounter := model.MsgCounterType(500) + + msg := &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(msgCounter), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: util.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + data = s.sut.PendingConsumptionLimits() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyConsumptionLimit(model.MsgCounterType(499), true, "") + + s.sut.ApproveOrDenyConsumptionLimit(msgCounter, false, "leave me alone") +} + +func (s *LPCSuite) Test_Failsafe() { + limit, changeable, err := s.sut.FailsafeConsumptionActivePowerLimit() + assert.Equal(s.T(), 0.0, limit) + assert.Equal(s.T(), false, changeable) + assert.Nil(s.T(), err) + + err = s.sut.SetFailsafeConsumptionActivePowerLimit(10, true) + assert.Nil(s.T(), err) + + limit, changeable, err = s.sut.FailsafeConsumptionActivePowerLimit() + 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.Nil(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.FailsafeConsumptionActivePowerLimit() + 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 *LPCSuite) Test_IsHeartbeatWithinDuration() { + assert.Nil(s.T(), s.sut.heartbeatDiag) + + value := s.sut.IsHeartbeatWithinDuration() + assert.False(s.T(), value) + + remoteDiagServer := s.monitoredEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + assert.NotNil(s.T(), remoteDiagServer) + + var err error + s.sut.heartbeatDiag, err = client.NewDeviceDiagnosis(s.sut.LocalEntity, s.monitoredEntity) + assert.NotNil(s.T(), remoteDiagServer) + assert.Nil(s.T(), err) + + // add heartbeat data to the remoteDiagServer + timestamp := time.Now().Add(-time.Second * 121) + data := &model.DeviceDiagnosisHeartbeatDataType{ + Timestamp: model.NewAbsoluteOrRelativeTimeTypeFromTime(timestamp), + HeartbeatCounter: util.Ptr(uint64(1)), + HeartbeatTimeout: model.NewDurationType(time.Second * 120), + } + err1 := remoteDiagServer.UpdateData(model.FunctionTypeDeviceDiagnosisHeartbeatData, data, nil, nil) + assert.Nil(s.T(), err1) + + value = s.sut.IsHeartbeatWithinDuration() + assert.False(s.T(), value) + + timestamp = time.Now() + data.Timestamp = model.NewAbsoluteOrRelativeTimeTypeFromTime(timestamp) + + err1 = remoteDiagServer.UpdateData(model.FunctionTypeDeviceDiagnosisHeartbeatData, data, nil, nil) + assert.Nil(s.T(), err1) + + value = s.sut.IsHeartbeatWithinDuration() + assert.True(s.T(), value) +} + +func (s *LPCSuite) Test_ContractualConsumptionNominalMax() { + value, err := s.sut.ContractualConsumptionNominalMax() + assert.Equal(s.T(), 0.0, value) + assert.NotNil(s.T(), err) + + err = s.sut.SetContractualConsumptionNominalMax(10) + assert.Nil(s.T(), err) + + value, err = s.sut.ContractualConsumptionNominalMax() + assert.Equal(s.T(), 10.0, value) + assert.Nil(s.T(), err) +} diff --git a/usecases/cs/lpc/testhelper_test.go b/usecases/cs/lpc/testhelper_test.go new file mode 100644 index 00000000..c45c12a7 --- /dev/null +++ b/usecases/cs/lpc/testhelper_test.go @@ -0,0 +1,200 @@ +package lpc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPCSuite(t *testing.T) { + suite.Run(t, new(LPCSuite)) +} + +type LPCSuite struct { + suite.Suite + + sut *CsLPC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface + + eventCalled bool +} + +func (s *LPCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *LPCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCsLPC(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + 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 api.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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.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/usecases/cs/lpc/types.go b/usecases/cs/lpc/types.go new file mode 100644 index 00000000..aa44a4ba --- /dev/null +++ b/usecases/cs/lpc/types.go @@ -0,0 +1,42 @@ +package lpc + +import "github.com/enbility/eebus-go/api" + +const ( + // Load control obligation limit data update received + // + // Use `ConsumptionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "cs-lpc-DataUpdateLimit" + + // An incoming load control obligation limit needs to be approved or denied + // + // Use `PendingConsumptionLimits` to get the currently pending write approval requests + // and invoke `ApproveOrDenyConsumptionLimit` for each + // + // Use Case LPC, Scenario 1 + WriteApprovalRequired api.EventType = "cs-lpc-WriteApprovalRequired" + + // Failsafe limit for the consumed active (real) power of the + // Controllable System data update received + // + // Use `FailsafeConsumptionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeConsumptionActivePowerLimit api.EventType = "cs-lpc-DataUpdateFailsafeConsumptionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data update received + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "cs-lpc-DataUpdateFailsafeDurationMinimum" + + // Indicates a notify heartbeat event the application should care of. + // E.g. going into or out of the Failsafe state + // + // Use Case LPC, Scenario 3 + DataUpdateHeartbeat api.EventType = "cs-lpc-DataUpdateHeartbeat" +) diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go new file mode 100644 index 00000000..82c00909 --- /dev/null +++ b/usecases/cs/lpc/usecase.go @@ -0,0 +1,246 @@ +package lpc + +import ( + "sync" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + features "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/features/server" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CsLPC struct { + *usecase.UseCaseBase + + pendingMux sync.Mutex + pendingLimits map[model.MsgCounterType]*spineapi.Message + + heartbeatDiag *features.DeviceDiagnosis + + heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use +} + +var _ ucapi.CsLPCInterface = (*CsLPC)(nil) + +func NewCsLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CsLPC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeGridGuard, + model.EntityTypeTypeCEM, // KEO uses this entity type for an SMGW whysoever + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeControllableSystem, + model.UseCaseNameTypeLimitationOfPowerConsumption, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + eventCB, + validEntityTypes, + ) + + uc := &CsLPC{ + UseCaseBase: usecase, + pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CsLPC) loadControlServerAndLimitId() (lc *server.LoadControl, limitid model.LoadControlLimitIdType, err error) { + limitid = model.LoadControlLimitIdType(0) + + lc, err = server.NewLoadControl(e.LocalEntity) + if err != nil { + return + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + descriptions, err := lc.GetLimitDescriptionsForFilter(filter) + if err != nil || len(descriptions) != 1 || descriptions[0].LimitId == nil { + return + } + description := descriptions[0] + + if description.LimitId == nil { + return + } + + return lc, *description.LimitId, nil +} + +// callback invoked on incoming write messages to this +// loadcontrol server feature. +// the implementation only considers write messages for this use case and +// approves all others +func (e *CsLPC) loadControlWriteCB(msg *spineapi.Message) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.LoadControlLimitListData == nil { + return + } + + _, limitId, err := e.loadControlServerAndLimitId() + if err != nil { + return + } + + data := msg.Cmd.LoadControlLimitListData + + // we assume there is always only one limit + if data == nil || data.LoadControlLimitData == nil || + len(data.LoadControlLimitData) == 0 { + return + } + + // check if there is a matching limitId in the data + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return + } + } + + // approve, because this is no request for this usecase + go e.ApproveOrDenyConsumptionLimit(*msg.RequestHeader.MsgCounter, true, "") +} + +func (e *CsLPC) AddFeatures() { + // client features + _ = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + // server features + f := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + _ = f.AddWriteApprovalCallback(e.loadControlWriteCB) + + newLimitDesc := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + MeasurementId: util.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: util.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + feat, _ := server.NewLoadControl(e.LocalEntity) + feat.AddLimitDescription(newLimitDesc) + + f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + + if dcs, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { + dcs.AddKeyValueDescription( + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + ) + dcs.AddKeyValueDescription( + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + ) + + value := &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(false), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + ) + + value = &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(false), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + ) + } + + f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) + + f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, false) + + if ec, err := server.NewElectricalConnection(e.LocalEntity); err == nil { + // ElectricalConnectionId and ParameterId should be identical to the ones used + // in a MPC Server role implementation, which is not done here (yet) + newCharData := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualConsumptionNominalMax), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + } + _, _ = ec.AddCharacteristic(newCharData) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CsLPC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceDiagnosis(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/cs/lpc/usecase_test.go b/usecases/cs/lpc/usecase_test.go new file mode 100644 index 00000000..fe23a2ad --- /dev/null +++ b/usecases/cs/lpc/usecase_test.go @@ -0,0 +1,117 @@ +package lpc + +import ( + "time" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPCSuite) Test_loadControlServerAndLimitId() { + lc, _, err := s.sut.loadControlServerAndLimitId() + assert.NotNil(s.T(), lc) + assert.Nil(s.T(), err) + + f := s.sut.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, &model.LoadControlLimitDescriptionListDataType{}, nil, nil) + lc, _, err = s.sut.loadControlServerAndLimitId() + assert.NotNil(s.T(), lc) + assert.NotNil(s.T(), err) + + s.sut.LocalEntity = nil + lc, _, err = s.sut.loadControlServerAndLimitId() + assert.Nil(s.T(), lc) + assert.NotNil(s.T(), err) +} + +func (s *LPCSuite) Test_loadControlWriteCB() { + msg := &spineapi.Message{} + + s.sut.loadControlWriteCB(msg) + + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(500)), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{}, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + {}, + }, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: util.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + } + + s.sut.loadControlWriteCB(msg) +} + +func (s *LPCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *LPCSuite) 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: util.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeLimitationOfPowerConsumption), + UseCaseAvailable: util.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/usecases/cs/lpp/events.go b/usecases/cs/lpp/events.go new file mode 100644 index 00000000..1380d417 --- /dev/null +++ b/usecases/cs/lpp/events.go @@ -0,0 +1,171 @@ +package lpp + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/features/server" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *CsLPP) HandleEvent(payload spineapi.EventPayload) { + if internal.IsDeviceConnected(payload) { + e.deviceConnected(payload) + return + } + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + // 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 internal.IsHeartbeat(payload) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateHeartbeat) + return + } + + if 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 := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + if payload.Function != model.FunctionTypeLoadControlLimitListData || + payload.LocalFeature != serverF { + return + } + + e.loadControlLimitDataUpdate(payload) + case *model.DeviceConfigurationKeyValueListDataType: + serverF := e.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 *CsLPP) 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 !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceDiagnosis(e.LocalEntity, deviceDiagEntites[0]); err == nil { + e.heartbeatDiag = localDeviceDiag + 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 *CsLPP) subscribeHeartbeatWorkaround(payload spineapi.EventPayload) { + // is the workaround is needed? + if e.heartbeatKeoWorkaround { + if localDeviceDiag, err := client.NewDeviceDiagnosis(e.LocalEntity, payload.Entity); err == nil { + e.heartbeatDiag = localDeviceDiag + 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 *CsLPP) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if lc, err := server.NewLoadControl(e.LocalEntity); err == nil { + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + } + if lc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } + } +} + +// the configuration key data was updated +func (e *CsLPP) configurationDataUpdate(payload spineapi.EventPayload) { + if dc, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeProductionActivePowerLimit) + } + filter = model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } + } +} diff --git a/usecases/cs/lpp/events_test.go b/usecases/cs/lpp/events_test.go new file mode 100644 index 00000000..b0def229 --- /dev/null +++ b/usecases/cs/lpp/events_test.go @@ -0,0 +1,332 @@ +package lpp + +import ( + "fmt" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPPSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + EventType: spineapi.EventTypeSubscriptionChange, + } + 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 = util.Ptr(model.CmdClassifierTypeWrite) + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeLoadControlLimitListData + payload.Data = util.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.LocalFeature = s.loadControlFeature + s.sut.HandleEvent(payload) + + payload.Function = model.FunctionTypeDeviceConfigurationKeyValueListData + payload.Data = util.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) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Function = model.FunctionTypeDeviceDiagnosisHeartbeatData + payload.LocalFeature = s.deviceDiagnosisFeature + payload.CmdClassifier = util.Ptr(model.CmdClassifierTypeNotify) + payload.Data = util.Ptr(model.DeviceDiagnosisHeartbeatDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *LPPSuite) 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 *LPPSuite) 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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{model.AddressEntityType(i)}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{2}, + }, + EntityType: util.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{3}, + }, + EntityType: util.Ptr(model.EntityTypeTypeCEM), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{4}, + }, + EntityType: util.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) +} + +func (s *LPPSuite) Test_loadControlLimitDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + }, + }, + } + + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + lFeature.SetData(model.FunctionTypeLoadControlLimitDescriptionListData, descData) + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *LPPSuite) Test_configurationDataUpdate() { + localDevice := s.service.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + lFeature := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + LocalFeature: lFeature, + } + + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + }, + } + + lFeature.SetData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData) + + s.eventCalled = false + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.eventCalled = false + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: util.Ptr(model.DeviceConfigurationKeyValueValueType{}), + }, + }, + } + + payload.Data = data + + s.eventCalled = false + s.sut.configurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go new file mode 100644 index 00000000..fbf5e798 --- /dev/null +++ b/usecases/cs/lpp/public.go @@ -0,0 +1,332 @@ +package lpp + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/server" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// 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 *CsLPP) ProductionLimit() (limit ucapi.LoadLimit, resultErr error) { + limit = ucapi.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + Duration: 0, + } + resultErr = api.ErrDataNotAvailable + + lc, limidId, err := e.loadControlServerAndLimitId() + if err != nil { + return limit, err + } + + value, err := lc.GetLimitDataForId(limidId) + if err != nil || value == nil || 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 *CsLPP) SetProductionLimit(limit ucapi.LoadLimit) (resultErr error) { + loadControlf, limidId, err := e.loadControlServerAndLimitId() + if err != nil { + return err + } + + limitData := model.LoadControlLimitDataType{ + LimitId: util.Ptr(limidId), + IsLimitChangeable: util.Ptr(limit.IsChangeable), + IsLimitActive: util.Ptr(limit.IsActive), + Value: model.NewScaledNumberType(limit.Value), + } + if limit.Duration > 0 { + limitData.TimePeriod = &model.TimePeriodType{ + EndTime: model.NewAbsoluteOrRelativeTimeTypeFromDuration(limit.Duration), + } + } + + deleteTimePeriod := &model.LoadControlLimitDataElementsType{ + TimePeriod: util.Ptr(model.TimePeriodElementsType{}), + } + + return loadControlf.UpdateLimitDataForId(limitData, deleteTimePeriod, limidId) +} + +// return the currently pending incoming consumption write limits +func (e *CsLPP) PendingProductionLimits() map[model.MsgCounterType]ucapi.LoadLimit { + result := make(map[model.MsgCounterType]ucapi.LoadLimit) + + _, limitId, err := e.loadControlServerAndLimitId() + if err != nil { + return result + } + + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + for key, msg := range e.pendingLimits { + data := msg.Cmd.LoadControlLimitListData + + // elements are only added to the map if all required fields exist + // therefor not check for these are needed here + + // find the item which contains the limit for this usecase + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + limit := ucapi.LoadLimit{} + + if item.TimePeriod != nil { + if duration, err := item.TimePeriod.GetDuration(); err == nil { + limit.Duration = duration + } + } + + if item.IsLimitActive != nil { + limit.IsActive = *item.IsLimitActive + } + + if item.Value != nil { + limit.Value = item.Value.GetValue() + } + + result[key] = limit + } + } + + return result +} + +// accept or deny an incoming consumption write limit +// +// use PendingProductionLimits to get the list of currently pending requests +func (e *CsLPP) ApproveOrDenyProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + msg, ok := e.pendingLimits[msgCounter] + if !ok { + return + } + + f := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + + result := model.ErrorType{ + ErrorNumber: model.ErrorNumberType(0), + } + if !approve { + result.ErrorNumber = model.ErrorNumberType(7) + result.Description = util.Ptr(model.DescriptionType(reason)) + } + f.ApproveOrDenyWrite(msg, result) +} + +// 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 *CsLPP) FailsafeProductionActivePowerLimit() (limit float64, isChangeable bool, resultErr error) { + limit = 0 + isChangeable = false + resultErr = api.ErrDataNotAvailable + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + } + keyData, err := dc.GetKeyValueDataForFilter(filter) + if err != nil || keyData == nil || 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 *CsLPP) SetFailsafeProductionActivePowerLimit(value float64, changeable bool) error { + keyName := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + keyValue := model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(value), + } + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return err + } + + data := model.DeviceConfigurationKeyValueDataType{ + Value: &keyValue, + IsValueChangeable: &changeable, + } + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(keyName), + } + return dc.UpdateKeyValueDataForFilter(data, nil, filter) +} + +// return minimum time the Controllable System remains in "failsafe state" unless conditions +// specified in this Use Case permit leaving the "failsafe state" +func (e *CsLPP) FailsafeDurationMinimum() (duration time.Duration, isChangeable bool, resultErr error) { + duration = 0 + isChangeable = false + resultErr = api.ErrDataNotAvailable + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + } + keyData, err := dc.GetKeyValueDataForFilter(filter) + if err != nil || keyData == nil || 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 *CsLPP) 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), + } + + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return err + } + + data := model.DeviceConfigurationKeyValueDataType{ + Value: &keyValue, + IsValueChangeable: &changeable, + } + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(keyName), + } + return dc.UpdateKeyValueDataForFilter(data, nil, filter) +} + +// Scenario 3 + +func (e *CsLPP) IsHeartbeatWithinDuration() bool { + if e.heartbeatDiag == nil { + return false + } + + return e.heartbeatDiag.IsHeartbeatWithinDuration(2 * time.Minute) +} + +// Scenario 4 + +// return nominal maximum active (real) power the Controllable System is +// allowed to produce due to the customer's contract. +func (e *CsLPP) ContractualProductionNominalMax() (value float64, resultErr error) { + value = 0 + resultErr = api.ErrDataNotAvailable + + ec, err := server.NewElectricalConnection(e.LocalEntity) + if err != nil { + resultErr = err + return + } + + filter := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), + } + charData, err := ec.GetCharacteristicsForFilter(filter) + if err != nil || len(charData) == 0 || + charData[0].CharacteristicId == nil || + charData[0].Value == nil { + return + } + + return charData[0].Value.GetValue(), nil +} + +// set nominal maximum active (real) power the Controllable System is +// allowed to produce due to the customer's contract. +func (e *CsLPP) SetContractualProductionNominalMax(value float64) error { + ec, err := server.NewElectricalConnection(e.LocalEntity) + if err != nil { + return err + } + + electricalConnectionid := util.Ptr(model.ElectricalConnectionIdType(0)) + parameterId := util.Ptr(model.ElectricalConnectionParameterIdType(0)) + charList, err := ec.GetCharacteristicsForFilter(model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: electricalConnectionid, + ParameterId: parameterId, + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), + }) + if err != nil || len(charList) == 0 { + return api.ErrDataNotAvailable + } + + data := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: electricalConnectionid, + ParameterId: parameterId, + CharacteristicId: charList[0].CharacteristicId, + Value: model.NewScaledNumberType(value), + } + return ec.UpdateCharacteristic(data, nil) +} diff --git a/usecases/cs/lpp/public_test.go b/usecases/cs/lpp/public_test.go new file mode 100644 index 00000000..fe525d01 --- /dev/null +++ b/usecases/cs/lpp/public_test.go @@ -0,0 +1,154 @@ +package lpp + +import ( + "time" + + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPPSuite) Test_LoadControlLimit() { + limit, err := s.sut.ProductionLimit() + assert.Equal(s.T(), 0.0, limit.Value) + assert.NotNil(s.T(), err) + + newLimit := ucapi.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 *LPPSuite) Test_PendingProductionLimits() { + data := s.sut.PendingProductionLimits() + assert.Equal(s.T(), 0, len(data)) + + msgCounter := model.MsgCounterType(500) + + msg := &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(msgCounter), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: util.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + data = s.sut.PendingProductionLimits() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyProductionLimit(model.MsgCounterType(499), true, "") + + s.sut.ApproveOrDenyProductionLimit(msgCounter, false, "leave me alone") +} + +func (s *LPPSuite) Test_Failsafe() { + limit, changeable, err := s.sut.FailsafeProductionActivePowerLimit() + assert.Equal(s.T(), 0.0, limit) + assert.Equal(s.T(), false, changeable) + assert.Nil(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.Nil(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 *LPPSuite) Test_IsHeartbeatWithinDuration() { + assert.Nil(s.T(), s.sut.heartbeatDiag) + + value := s.sut.IsHeartbeatWithinDuration() + assert.False(s.T(), value) + + remoteDiagServer := s.monitoredEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + assert.NotNil(s.T(), remoteDiagServer) + + var err error + s.sut.heartbeatDiag, err = client.NewDeviceDiagnosis(s.sut.LocalEntity, s.monitoredEntity) + assert.NotNil(s.T(), remoteDiagServer) + assert.Nil(s.T(), err) + + // add heartbeat data to the remoteDiagServer + timestamp := time.Now().Add(-time.Second * 121) + data := &model.DeviceDiagnosisHeartbeatDataType{ + Timestamp: model.NewAbsoluteOrRelativeTimeTypeFromTime(timestamp), + HeartbeatCounter: util.Ptr(uint64(1)), + HeartbeatTimeout: model.NewDurationType(time.Second * 120), + } + err1 := remoteDiagServer.UpdateData(model.FunctionTypeDeviceDiagnosisHeartbeatData, data, nil, nil) + assert.Nil(s.T(), err1) + + value = s.sut.IsHeartbeatWithinDuration() + assert.False(s.T(), value) + + timestamp = time.Now() + data.Timestamp = model.NewAbsoluteOrRelativeTimeTypeFromTime(timestamp) + + err1 = remoteDiagServer.UpdateData(model.FunctionTypeDeviceDiagnosisHeartbeatData, data, nil, nil) + assert.Nil(s.T(), err1) + + value = s.sut.IsHeartbeatWithinDuration() + assert.True(s.T(), value) +} + +func (s *LPPSuite) 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/usecases/cs/lpp/testhelper_test.go b/usecases/cs/lpp/testhelper_test.go new file mode 100644 index 00000000..7fe735d9 --- /dev/null +++ b/usecases/cs/lpp/testhelper_test.go @@ -0,0 +1,200 @@ +package lpp + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPPSuite(t *testing.T) { + suite.Run(t, new(LPPSuite)) +} + +type LPPSuite struct { + suite.Suite + + sut *CsLPP + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface + + eventCalled bool +} + +func (s *LPPSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *LPPSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCsLPP(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + 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 api.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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.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/usecases/cs/lpp/types.go b/usecases/cs/lpp/types.go new file mode 100644 index 00000000..d744536b --- /dev/null +++ b/usecases/cs/lpp/types.go @@ -0,0 +1,42 @@ +package lpp + +import "github.com/enbility/eebus-go/api" + +const ( + // Load control obligation limit data update received + // + // Use `ProductionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "cs-lpp-DataUpdateLimit" + + // An incoming load control obligation limit needs to be approved or denied + // + // Use `PendingProductionLimits` to get the currently pending write approval requests + // and invoke `ApproveOrDenyProductionLimit` for each + // + // Use Case LPC, Scenario 1 + WriteApprovalRequired api.EventType = "cs-lpp-WriteApprovalRequired" + + // Failsafe limit for the produced active (real) power of the + // Controllable System data update received + // + // Use `FailsafeProductionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeProductionActivePowerLimit api.EventType = "cs-lpp-DataUpdateFailsafeProductionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data update received + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "cs-lpp-DataUpdateFailsafeDurationMinimum" + + // Indicates a notify heartbeat event the application should care of. + // E.g. going into or out of the Failsafe state + // + // Use Case LPP, Scenario 3 + DataUpdateHeartbeat api.EventType = "uclpcserver-DataUpdateHeartbeat" +) diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go new file mode 100644 index 00000000..0e7481e8 --- /dev/null +++ b/usecases/cs/lpp/usecase.go @@ -0,0 +1,247 @@ +package lpp + +import ( + "sync" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + features "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/features/server" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type CsLPP struct { + *usecase.UseCaseBase + + pendingMux sync.Mutex + pendingLimits map[model.MsgCounterType]*spineapi.Message + + heartbeatDiag *features.DeviceDiagnosis + + heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use +} + +var _ ucapi.CsLPPInterface = (*CsLPP)(nil) + +func NewCsLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CsLPP { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeGridGuard, + model.EntityTypeTypeCEM, // KEO uses this entity type for an SMGW whysoever + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeControllableSystem, + model.UseCaseNameTypeLimitationOfPowerProduction, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + eventCB, + validEntityTypes, + ) + + uc := &CsLPP{ + UseCaseBase: usecase, + pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *CsLPP) loadControlServerAndLimitId() (lc *server.LoadControl, limitid model.LoadControlLimitIdType, err error) { + limitid = model.LoadControlLimitIdType(0) + + lc, err = server.NewLoadControl(e.LocalEntity) + if err != nil { + return + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + descriptions, err := lc.GetLimitDescriptionsForFilter(filter) + if err != nil || len(descriptions) != 1 || descriptions[0].LimitId == nil { + return + } + description := descriptions[0] + + if description.LimitId == nil { + return + } + + return lc, *description.LimitId, nil +} + +// callback invoked on incoming write messages to this +// loadcontrol server feature. +// the implementation only considers write messages for this use case and +// approves all others +func (e *CsLPP) loadControlWriteCB(msg *spineapi.Message) { + e.pendingMux.Lock() + defer e.pendingMux.Unlock() + + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.LoadControlLimitListData == nil { + return + } + + _, limitId, err := e.loadControlServerAndLimitId() + if err != nil { + return + } + + data := msg.Cmd.LoadControlLimitListData + + // we assume there is always only one limit + if data == nil || data.LoadControlLimitData == nil || + len(data.LoadControlLimitData) == 0 { + return + } + + // check if there is a matching limitId in the data + for _, item := range data.LoadControlLimitData { + if item.LimitId == nil || + limitId != *item.LimitId { + continue + } + + if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return + } + } + + // approve, because this is no request for this usecase + go e.ApproveOrDenyProductionLimit(*msg.RequestHeader.MsgCounter, true, "") +} + +func (e *CsLPP) AddFeatures() { + // client features + _ = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) + + // server features + f := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true) + _ = f.AddWriteApprovalCallback(e.loadControlWriteCB) + + newLimitDesc := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + MeasurementId: util.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: util.Ptr(model.UnitOfMeasurementTypeW), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + if lc, err := server.NewLoadControl(e.LocalEntity); err == nil { + lc.AddLimitDescription(newLimitDesc) + } + + f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + + if dcs, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { + dcs.AddKeyValueDescription( + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + ) + dcs.AddKeyValueDescription( + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + ) + + value := &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(false), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + ) + + value = &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(false), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + ) + } + + f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) + + f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeElectricalConnectionCharacteristicListData, true, false) + + if ec, err := server.NewElectricalConnection(e.LocalEntity); err == nil { + // ElectricalConnectionId and ParameterId should be identical to the ones used + // in a MPC Server role implementation, which is not done here (yet) + newCharData := model.ElectricalConnectionCharacteristicDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypeContractualProductionNominalMax), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + } + _, _ = ec.AddCharacteristic(newCharData) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *CsLPP) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceDiagnosis(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/cs/lpp/usecase_test.go b/usecases/cs/lpp/usecase_test.go new file mode 100644 index 00000000..c71f09d4 --- /dev/null +++ b/usecases/cs/lpp/usecase_test.go @@ -0,0 +1,117 @@ +package lpp + +import ( + "time" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPPSuite) Test_loadControlServerAndLimitId() { + lc, _, err := s.sut.loadControlServerAndLimitId() + assert.NotNil(s.T(), lc) + assert.Nil(s.T(), err) + + f := s.sut.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + f.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, &model.LoadControlLimitDescriptionListDataType{}, nil, nil) + lc, _, err = s.sut.loadControlServerAndLimitId() + assert.NotNil(s.T(), lc) + assert.NotNil(s.T(), err) + + s.sut.LocalEntity = nil + lc, _, err = s.sut.loadControlServerAndLimitId() + assert.Nil(s.T(), lc) + assert.NotNil(s.T(), err) +} + +func (s *LPPSuite) Test_loadControlWriteCB() { + msg := &spineapi.Message{} + + s.sut.loadControlWriteCB(msg) + + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(500)), + }, + Cmd: model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{}, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + {}, + }, + }, + } + + s.sut.loadControlWriteCB(msg) + + msg.Cmd = model.CmdType{ + LoadControlLimitListData: &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitActive: util.Ptr(true), + Value: model.NewScaledNumberType(1000), + TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2), + }, + }, + }, + } + + s.sut.loadControlWriteCB(msg) +} + +func (s *LPPSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *LPPSuite) 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: util.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeLimitationOfPowerProduction), + UseCaseAvailable: util.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/usecases/eg/lpc/events.go b/usecases/eg/lpc/events.go new file mode 100644 index 00000000..1e67a81f --- /dev/null +++ b/usecases/eg/lpc/events.go @@ -0,0 +1,110 @@ +package lpc + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *EgLPC) HandleEvent(payload spineapi.EventPayload) { + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.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 *EgLPC) connected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if loadControl, err := client.NewLoadControl(e.LocalEntity, 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 := client.NewDeviceDiagnosis(e.LocalEntity, entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data was updated +func (e *EgLPC) loadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if loadControl, err := client.NewLoadControl(e.LocalEntity, entity); err == nil { + // get values + if _, err := loadControl.RequestLimitData(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *EgLPC) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if lc, err := client.NewLoadControl(e.LocalEntity, payload.Entity); err == nil { + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + if lc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } + } +} + +// the configuration key description data was updated +func (e *EgLPC) configurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, 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 *EgLPC) configurationDataUpdate(payload spineapi.EventPayload) { + if dc, err := client.NewDeviceConfiguration(e.LocalEntity, payload.Entity); err == nil { + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeConsumptionActivePowerLimit) + } + filter.KeyName = util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum) + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } + } +} diff --git a/usecases/eg/lpc/events_test.go b/usecases/eg/lpc/events_test.go new file mode 100644 index 00000000..f3255d6a --- /dev/null +++ b/usecases/eg/lpc/events_test.go @@ -0,0 +1,155 @@ +package lpc + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPCSuite) 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 = util.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *LPCSuite) Test_Failures() { + s.sut.connected(s.mockRemoteEntity) + + s.sut.configurationDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *LPCSuite) Test_loadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.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) + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *LPCSuite) Test_configurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: util.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) + + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/eg/lpc/public.go b/usecases/eg/lpc/public.go new file mode 100644 index 00000000..08ae9670 --- /dev/null +++ b/usecases/eg/lpc/public.go @@ -0,0 +1,311 @@ +package lpc + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// 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 *EgLPC) ConsumptionLimit(entity spineapi.EntityRemoteInterface) ( + limit ucapi.LoadLimit, resultErr error) { + limit = ucapi.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + } + + resultErr = api.ErrNoCompatibleEntity + if !e.IsCompatibleEntity(entity) { + return + } + + resultErr = api.ErrDataNotAvailable + loadControl, err := client.NewLoadControl(e.LocalEntity, entity) + if err != nil || loadControl == nil { + return + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + limitDescriptions, err := loadControl.GetLimitDescriptionsForFilter(filter) + if err != nil || len(limitDescriptions) != 1 { + return + } + + value, err := loadControl.GetLimitDataForId(*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 *EgLPC) WriteConsumptionLimit( + entity spineapi.EntityRemoteInterface, + limit ucapi.LoadLimit) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + loadControl, err := client.NewLoadControl(e.LocalEntity, entity) + if err != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + limitDescriptions, err := loadControl.GetLimitDescriptionsForFilter(filter) + if err != nil || len(limitDescriptions) != 1 || + limitDescriptions[0].LimitId == nil { + return nil, api.ErrMetadataNotAvailable + } + + limitDesc := limitDescriptions[0] + + if _, err := loadControl.GetLimitDataForId(*limitDesc.LimitId); err != nil { + return nil, api.ErrDataNotAvailable + } + + currentLimits, err := loadControl.GetLimitDataForFilter(model.LoadControlLimitDescriptionDataType{}) + if err != nil { + return nil, api.ErrDataNotAvailable + } + + for index, item := range currentLimits { + if item.LimitId == nil || + *item.LimitId != *limitDesc.LimitId { + continue + } + + // EEBus_UC_TS_LimitationOfPowerConsumption 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, api.ErrNotSupported + } + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: util.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.WriteLimitData(limitData) + + return msgCounter, err +} + +// Scenario 2 + +// return Failsafe limit for the consumed active (real) power of the +// Controllable System. This limit becomes activated in "init" state or "failsafe state". +func (e *EgLPC) FailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return 0, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil || data.Value == nil || data.Value.ScaledNumber == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.ScaledNumber.GetValue(), nil +} + +// send new Failsafe Consumption Active Power Limit +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - value: the new limit in W +func (e *EgLPC) WriteFailsafeConsumptionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return nil, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + } + data, err := deviceConfiguration.GetKeyValueDescriptionsForFilter(filter) + if err != nil || data == nil || len(data) != 1 { + return nil, api.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data[0].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 *EgLPC) FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return 0, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil || data.Value == nil || data.Value.Duration == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.Duration.GetTimeDuration() +} + +// send new Failsafe Duration Minimum +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - duration: the duration, between 2h and 24h +func (e *EgLPC) WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return nil, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil { + return nil, api.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 consume according to the device label or data sheet. +func (e *EgLPC) PowerConsumptionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil || electricalConnection == nil { + return 0, err + } + + filter := model.ElectricalConnectionCharacteristicDataType{ + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypePowerConsumptionNominalMax), + } + data, err := electricalConnection.GetCharacteristicsForFilter(filter) + if err != nil || len(data) == 0 || data[0].Value == nil { + return 0, err + } + + return data[0].Value.GetValue(), nil +} diff --git a/usecases/eg/lpc/public_test.go b/usecases/eg/lpc/public_test.go new file mode 100644 index 00000000..34d939d6 --- /dev/null +++ b/usecases/eg/lpc/public_test.go @@ -0,0 +1,363 @@ +package lpc + +import ( + "time" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPCSuite) Test_ConsumptionLimit() { + data, err := s.sut.ConsumptionLimit(nil) + 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.ConsumptionLimit(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.ConsumptionLimit(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: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.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.ConsumptionLimit(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: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: util.Ptr(true), + IsLimitActive: util.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.ConsumptionLimit(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 *LPCSuite) Test_WriteLoadControlLimit() { + limit := ucapi.LoadLimit{ + Value: 6000, + IsActive: true, + Duration: 0, + } + _, err := s.sut.WriteConsumptionLimit(s.mockRemoteEntity, limit) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: util.Ptr(model.EnergyDirectionTypeConsume), + ScopeType: util.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.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: util.Ptr(true), + IsLimitActive: util.Ptr(false), + Value: model.NewScaledNumberType(6000), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) + + limit.Duration = time.Duration(time.Hour * 2) + _, err = s.sut.WriteConsumptionLimit(s.monitoredEntity, limit) + assert.NotNil(s.T(), err) +} + +func (s *LPCSuite) Test_FailsafeConsumptionActivePowerLimit() { + data, err := s.sut.FailsafeConsumptionActivePowerLimit(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + 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.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.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.FailsafeConsumptionActivePowerLimit(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 4000.0, data) +} + +func (s *LPCSuite) Test_WriteFailsafeConsumptionActivePowerLimit() { + _, err := s.sut.WriteFailsafeConsumptionActivePowerLimit(s.mockRemoteEntity, 6000) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteFailsafeConsumptionActivePowerLimit(s.monitoredEntity, 6000) + assert.NotNil(s.T(), err) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + }, + } + + 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.WriteFailsafeConsumptionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + _, err = s.sut.WriteFailsafeConsumptionActivePowerLimit(s.monitoredEntity, 6000) + assert.Nil(s.T(), err) +} + +func (s *LPCSuite) 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: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + }, + } + + 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: util.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: util.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 *LPCSuite) 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: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.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.NotNil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.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 *LPCSuite) Test_PowerConsumptionNominalMax() { + data, err := s.sut.PowerConsumptionNominalMax(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerConsumptionNominalMax(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + charData := &model.ElectricalConnectionCharacteristicListDataType{ + ElectricalConnectionCharacteristicData: []model.ElectricalConnectionCharacteristicDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + CharacteristicId: util.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypePowerConsumptionNominalMax), + 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.PowerConsumptionNominalMax(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 8000.0, data) +} diff --git a/usecases/eg/lpc/testhelper_test.go b/usecases/eg/lpc/testhelper_test.go new file mode 100644 index 00000000..74f58f02 --- /dev/null +++ b/usecases/eg/lpc/testhelper_test.go @@ -0,0 +1,186 @@ +package lpc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPCSuite(t *testing.T) { + suite.Run(t, new(LPCSuite)) +} + +type LPCSuite struct { + suite.Suite + + sut *EgLPC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *LPCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *LPCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewEgLPC(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.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/usecases/eg/lpc/types.go b/usecases/eg/lpc/types.go new file mode 100644 index 00000000..2a4b0ca9 --- /dev/null +++ b/usecases/eg/lpc/types.go @@ -0,0 +1,28 @@ +package lpc + +import "github.com/enbility/eebus-go/api" + +const ( + // Load control obligation limit data updated + // + // Use `ConsumptionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "eg-lpc-DataUpdateLimit" + + // Failsafe limit for the consumed active (real) power of the + // Controllable System data updated + // + // Use `FailsafeConsumptionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeConsumptionActivePowerLimit api.EventType = "eg-lpc-DataUpdateFailsafeConsumptionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data updated + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "eg-lpc-DataUpdateFailsafeDurationMinimum" +) diff --git a/usecases/eg/lpc/usecase.go b/usecases/eg/lpc/usecase.go new file mode 100644 index 00000000..e6204f75 --- /dev/null +++ b/usecases/eg/lpc/usecase.go @@ -0,0 +1,103 @@ +package lpc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + usecase "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type EgLPC struct { + *usecase.UseCaseBase +} + +var _ ucapi.EgLPCInterface = (*EgLPC)(nil) + +func NewEgLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *EgLPC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + model.EntityTypeTypeEVSE, + model.EntityTypeTypeHeatPumpAppliance, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeEnergyGuard, + model.UseCaseNameTypeLimitationOfPowerConsumption, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + eventCB, + validEntityTypes) + + uc := &EgLPC{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *EgLPC) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *EgLPC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceDiagnosis(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + if _, err := client.NewLoadControl(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + if _, err := client.NewDeviceConfiguration(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/eg/lpc/usecase_test.go b/usecases/eg/lpc/usecase_test.go new file mode 100644 index 00000000..57a42cbe --- /dev/null +++ b/usecases/eg/lpc/usecase_test.go @@ -0,0 +1,45 @@ +package lpc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *LPCSuite) 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: util.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeLimitationOfPowerConsumption), + UseCaseAvailable: util.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/usecases/eg/lpp/events.go b/usecases/eg/lpp/events.go new file mode 100644 index 00000000..c3f96d37 --- /dev/null +++ b/usecases/eg/lpp/events.go @@ -0,0 +1,110 @@ +package lpp + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *EgLPP) HandleEvent(payload spineapi.EventPayload) { + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.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 *EgLPP) connected(entity spineapi.EntityRemoteInterface) { + // initialise features, e.g. subscriptions, descriptions + if loadControl, err := client.NewLoadControl(e.LocalEntity, 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 := client.NewDeviceDiagnosis(e.LocalEntity, entity); err == nil { + if _, err := localDeviceDiag.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit description data was updated +func (e *EgLPP) loadControlLimitDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if loadControl, err := client.NewLoadControl(e.LocalEntity, entity); err == nil { + // get values + if _, err := loadControl.RequestLimitData(); err != nil { + logging.Log().Debug(err) + } + } +} + +// the load control limit data was updated +func (e *EgLPP) loadControlLimitDataUpdate(payload spineapi.EventPayload) { + if lc, err := client.NewLoadControl(e.LocalEntity, payload.Entity); err == nil { + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + if lc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateLimit) + } + } +} + +// the configuration key description data was updated +func (e *EgLPP) configurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, 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 *EgLPP) configurationDataUpdate(payload spineapi.EventPayload) { + if dc, err := client.NewDeviceConfiguration(e.LocalEntity, payload.Entity); err == nil { + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeProductionActivePowerLimit) + } + filter.KeyName = util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum) + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFailsafeDurationMinimum) + } + } +} diff --git a/usecases/eg/lpp/events_test.go b/usecases/eg/lpp/events_test.go new file mode 100644 index 00000000..4303c26e --- /dev/null +++ b/usecases/eg/lpp/events_test.go @@ -0,0 +1,155 @@ +package lpp + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPPSuite) 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 = util.Ptr(model.LoadControlLimitDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.LoadControlLimitListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.DeviceConfigurationKeyValueListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *LPPSuite) Test_Failures() { + s.sut.connected(s.mockRemoteEntity) + + s.sut.configurationDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *LPPSuite) Test_loadControlLimitDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.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) + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{}, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + }, + } + + payload.Data = data + + s.sut.loadControlLimitDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *LPPSuite) Test_configurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + KeyName: util.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) + + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{}, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data = &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + Value: &model.DeviceConfigurationKeyValueValueType{}, + }, + }, + } + + payload.Data = data + + s.sut.configurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/eg/lpp/public.go b/usecases/eg/lpp/public.go new file mode 100644 index 00000000..28b16172 --- /dev/null +++ b/usecases/eg/lpp/public.go @@ -0,0 +1,311 @@ +package lpp + +import ( + "errors" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// 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 *EgLPP) ProductionLimit(entity spineapi.EntityRemoteInterface) ( + limit ucapi.LoadLimit, resultErr error) { + limit = ucapi.LoadLimit{ + Value: 0.0, + IsChangeable: false, + IsActive: false, + } + + resultErr = api.ErrNoCompatibleEntity + if !e.IsCompatibleEntity(entity) { + return + } + + resultErr = api.ErrDataNotAvailable + loadControl, err := client.NewLoadControl(e.LocalEntity, entity) + if err != nil || loadControl == nil { + return + } + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + limitDescriptions, err := loadControl.GetLimitDescriptionsForFilter(filter) + if err != nil || len(limitDescriptions) != 1 { + return + } + + value, err := loadControl.GetLimitDataForId(*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 *EgLPP) WriteProductionLimit( + entity spineapi.EntityRemoteInterface, + limit ucapi.LoadLimit) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + loadControl, err := client.NewLoadControl(e.LocalEntity, entity) + if err != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.Ptr(model.ScopeTypeTypeActivePowerLimit), + } + limitDescriptions, err := loadControl.GetLimitDescriptionsForFilter(filter) + if err != nil || len(limitDescriptions) != 1 || + limitDescriptions[0].LimitId == nil { + return nil, api.ErrMetadataNotAvailable + } + + limitDesc := limitDescriptions[0] + + if _, err := loadControl.GetLimitDataForId(*limitDesc.LimitId); err != nil { + return nil, api.ErrDataNotAvailable + } + + currentLimits, err := loadControl.GetLimitDataForFilter(model.LoadControlLimitDescriptionDataType{}) + if err != nil { + return nil, api.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, api.ErrNotSupported + } + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: util.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.WriteLimitData(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 *EgLPP) FailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return 0, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil || data.Value == nil || data.Value.ScaledNumber == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.ScaledNumber.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 *EgLPP) WriteFailsafeProductionActivePowerLimit(entity spineapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return nil, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + } + data, err := deviceConfiguration.GetKeyValueDescriptionsForFilter(filter) + if err != nil || data == nil || len(data) != 1 { + return nil, api.ErrDataNotAvailable + } + + keyData := []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: data[0].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 *EgLPP) FailsafeDurationMinimum(entity spineapi.EntityRemoteInterface) (time.Duration, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + keyname := model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return 0, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil || data.Value == nil || data.Value.Duration == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.Duration.GetTimeDuration() +} + +// send new Failsafe Duration Minimum +// +// parameters: +// - entity: the entity of the e.g. EVSE +// - duration: the duration, between 2h and 24h +func (e *EgLPP) WriteFailsafeDurationMinimum(entity spineapi.EntityRemoteInterface, duration time.Duration) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return nil, api.ErrDataNotAvailable + } + + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + } + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil { + return nil, api.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 *EgLPP) PowerProductionNominalMax(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil || electricalConnection == nil { + return 0, err + } + + filter := model.ElectricalConnectionCharacteristicDataType{ + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.Ptr(model.ElectricalConnectionCharacteristicTypeTypePowerProductionNominalMax), + } + data, err := electricalConnection.GetCharacteristicsForFilter(filter) + if err != nil || len(data) == 0 || data[0].Value == nil { + return 0, err + } + + return data[0].Value.GetValue(), nil +} diff --git a/usecases/eg/lpp/public_test.go b/usecases/eg/lpp/public_test.go new file mode 100644 index 00000000..952e893f --- /dev/null +++ b/usecases/eg/lpp/public_test.go @@ -0,0 +1,357 @@ +package lpp + +import ( + "time" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPPSuite) Test_LoadControlLimit() { + data, err := s.sut.ProductionLimit(nil) + 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: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.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: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: util.Ptr(true), + IsLimitActive: util.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 *LPPSuite) Test_WriteLoadControlLimit() { + limit := ucapi.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: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + LimitType: util.Ptr(model.LoadControlLimitTypeTypeSignDependentAbsValueLimit), + LimitDirection: util.Ptr(model.EnergyDirectionTypeProduce), + ScopeType: util.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: util.Ptr(model.LoadControlLimitIdType(0)), + IsLimitChangeable: util.Ptr(true), + IsLimitActive: util.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 *LPPSuite) 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: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + 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: util.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: util.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 *LPPSuite) 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: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.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: util.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 *LPPSuite) 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: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + }, + }, + } + + 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: util.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: util.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 *LPPSuite) 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: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.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.NotNil(s.T(), err) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.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 *LPPSuite) 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: util.Ptr(model.ElectricalConnectionIdType(0)), + CharacteristicId: util.Ptr(model.ElectricalConnectionCharacteristicIdType(0)), + CharacteristicContext: util.Ptr(model.ElectricalConnectionCharacteristicContextTypeEntity), + CharacteristicType: util.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/usecases/eg/lpp/testhelper_test.go b/usecases/eg/lpp/testhelper_test.go new file mode 100644 index 00000000..f591cbf8 --- /dev/null +++ b/usecases/eg/lpp/testhelper_test.go @@ -0,0 +1,186 @@ +package lpp + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestLPPSuite(t *testing.T) { + suite.Run(t, new(LPPSuite)) +} + +type LPPSuite struct { + suite.Suite + + sut *EgLPP + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *LPPSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *LPPSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewEgLPP(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.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: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.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/usecases/eg/lpp/types.go b/usecases/eg/lpp/types.go new file mode 100644 index 00000000..eb54d239 --- /dev/null +++ b/usecases/eg/lpp/types.go @@ -0,0 +1,28 @@ +package lpp + +import "github.com/enbility/eebus-go/api" + +const ( + // Load control obligation limit data updated + // + // Use `ProductionLimit` to get the current data + // + // Use Case LPC, Scenario 1 + DataUpdateLimit api.EventType = "eg-lpp-DataUpdateLimit" + + // Failsafe limit for the produced active (real) power of the + // Controllable System data updated + // + // Use `FailsafeProductionActivePowerLimit` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeProductionActivePowerLimit api.EventType = "eg-lpp-DataUpdateFailsafeProductionActivePowerLimit" + + // Minimum time the Controllable System remains in "failsafe state" unless conditions + // specified in this Use Case permit leaving the "failsafe state" data updated + // + // Use `FailsafeDurationMinimum` to get the current data + // + // Use Case LPC, Scenario 2 + DataUpdateFailsafeDurationMinimum api.EventType = "eg-lpp-DataUpdateFailsafeDurationMinimum" +) diff --git a/usecases/eg/lpp/usecase.go b/usecases/eg/lpp/usecase.go new file mode 100644 index 00000000..db6b0c61 --- /dev/null +++ b/usecases/eg/lpp/usecase.go @@ -0,0 +1,105 @@ +package lpp + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +type EgLPP struct { + *usecase.UseCaseBase +} + +var _ ucapi.EgLPPInterface = (*EgLPP)(nil) + +func NewEgLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *EgLPP { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeEVSE, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeEnergyGuard, + model.UseCaseNameTypeLimitationOfPowerProduction, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4}, + eventCB, + validEntityTypes) + + uc := &EgLPP{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *EgLPP) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceDiagnosis, + model.FeatureTypeTypeLoadControl, + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } + + // server features + f := e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceDiagnosisHeartbeatData, true, false) +} + +func (e *EgLPP) UpdateUseCaseAvailability(available bool) { + e.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 *EgLPP) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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 := client.NewDeviceDiagnosis(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + if _, err := client.NewLoadControl(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + if _, err := client.NewDeviceConfiguration(e.LocalEntity, entity); err != nil { + return false, api.ErrFunctionNotSupported + } + + return true, nil +} diff --git a/usecases/eg/lpp/usecase_test.go b/usecases/eg/lpp/usecase_test.go new file mode 100644 index 00000000..a6fb68c5 --- /dev/null +++ b/usecases/eg/lpp/usecase_test.go @@ -0,0 +1,45 @@ +package lpp + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *LPPSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *LPPSuite) 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: util.Ptr(model.UseCaseActorTypeEnergyGuard), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeLimitationOfPowerProduction), + UseCaseAvailable: util.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/usecases/gcp/mgcp/events.go b/usecases/gcp/mgcp/events.go new file mode 100644 index 00000000..e9444c3d --- /dev/null +++ b/usecases/gcp/mgcp/events.go @@ -0,0 +1,159 @@ +package mgcp + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *GcpMGCP) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.gridConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.DeviceConfigurationKeyValueDescriptionListDataType: + e.gridConfigurationDescriptionDataUpdate(payload.Entity) + case *model.DeviceConfigurationKeyValueListDataType: + e.gridConfigurationDataUpdate(payload) + case *model.MeasurementDescriptionListDataType: + e.gridMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.gridMeasurementDataUpdate(payload) + } +} + +// process required steps when a grid device is connected +func (e *GcpMGCP) gridConnected(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity); err == nil { + if _, err := deviceConfiguration.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get configuration data + if _, err := deviceConfiguration.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the configuration key description data of an SMGW was updated +func (e *GcpMGCP) gridConfigurationDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, 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 of an SMGW was updated +func (e *GcpMGCP) gridConfigurationDataUpdate(payload spineapi.EventPayload) { + if dc, err := client.NewDeviceConfiguration(e.LocalEntity, payload.Entity); err == nil { + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor), + } + if dc.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerLimitationFactor) + } + } +} + +// the measurement descriptiondata of an SMGW was updated +func (e *GcpMGCP) gridMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestData(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of an SMGW was updated +func (e *GcpMGCP) gridMeasurementDataUpdate(payload spineapi.EventPayload) { + if measurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + // Scenario 2 + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + // Scenario 3 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeGridFeedIn) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyFeedIn) + } + + // Scenario 4 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeGridConsumption) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyConsumed) + } + + // Scenario 5 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACCurrent) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentPerPhase) + } + + // Scenario 6 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACVoltage) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateVoltagePerPhase) + } + + // Scenario 7 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACFrequency) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFrequency) + } + } +} diff --git a/usecases/gcp/mgcp/events_test.go b/usecases/gcp/mgcp/events_test.go new file mode 100644 index 00000000..e9b9f36b --- /dev/null +++ b/usecases/gcp/mgcp/events_test.go @@ -0,0 +1,168 @@ +package mgcp + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MGCPSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.smgwEntity + 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 = util.Ptr(model.DeviceConfigurationKeyValueDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *MGCPSuite) Test_Failures() { + s.sut.gridConnected(s.mockRemoteEntity) + + s.sut.gridConfigurationDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.gridMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *MGCPSuite) Test_gridConfigurationDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.smgwEntity, + } + s.sut.gridConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.gridConfigurationDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + + payload.Data = keyData + + s.sut.gridConfigurationDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *MGCPSuite) Test_gridMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.smgwEntity, + } + s.sut.gridMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeGridFeedIn), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + ScopeType: util.Ptr(model.ScopeTypeTypeGridConsumption), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(3)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(4)), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(5)), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.gridMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(3)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(4)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(5)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.gridMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/gcp/mgcp/public.go b/usecases/gcp/mgcp/public.go new file mode 100644 index 00000000..f8141d7d --- /dev/null +++ b/usecases/gcp/mgcp/public.go @@ -0,0 +1,190 @@ +package mgcp + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// Scenario 1 + +// return the current power limitation factor +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *GcpMGCP) PowerLimitationFactor(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil || measurement == nil { + return 0, err + } + + keyname := model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor + + deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) + if err != nil || deviceConfiguration == nil { + return 0, err + } + + // check if device configuration description has curtailment limit factor key name + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: &keyname, + } + _, err = deviceConfiguration.GetKeyValueDescriptionsForFilter(filter) + if err != nil { + return 0, err + } + + filter.ValueType = util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber) + data, err := deviceConfiguration.GetKeyValueDataForFilter(filter) + if err != nil || data == nil || data.Value == nil || data.Value.ScaledNumber == nil { + return 0, api.ErrDataNotAvailable + } + + return data.Value.ScaledNumber.GetValue(), nil +} + +// Scenario 2 + +// return the momentary power consumption or production at the grid connection point +// +// - positive values are used for consumption +// - negative values are used for production +func (e *GcpMGCP) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + data, err := internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, nil) + if err != nil || len(data) != 1 { + return 0, api.ErrDataNotAvailable + } + + return data[0], nil +} + +// Scenario 3 + +// return the total feed in energy at the grid connection point +// +// - negative values are used for production +func (e *GcpMGCP) EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil || measurement == nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeGridFeedIn), + } + result, err := measurement.GetDataForFilter(filter) + if err != nil || len(result) == 0 || result[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + return result[0].Value.GetValue(), nil +} + +// Scenario 4 + +// return the total consumption energy at the grid connection point +// +// - positive values are used for consumption +func (e *GcpMGCP) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil || measurement == nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeGridConsumption), + } + result, err := measurement.GetDataForFilter(filter) + if err != nil || len(result) == 0 || result[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + return result[0].Value.GetValue(), nil +} + +// Scenario 5 + +// return the momentary current consumption or production at the grid connection point +// +// - positive values are used for consumption +// - negative values are used for production +func (e *GcpMGCP) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + } + return internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, internal.PhaseNameMapping) +} + +// Scenario 6 + +// return the voltage phase details at the grid connection point +func (e *GcpMGCP) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + } + return internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, "", internal.PhaseNameMapping) +} + +// Scenario 7 + +// return frequency at the grid connection point +func (e *GcpMGCP) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil || measurement == nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + } + result, err := measurement.GetDataForFilter(filter) + if err != nil || len(result) == 0 || result[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + return result[0].Value.GetValue(), nil +} diff --git a/usecases/gcp/mgcp/public_test.go b/usecases/gcp/mgcp/public_test.go new file mode 100644 index 00000000..de4b26c7 --- /dev/null +++ b/usecases/gcp/mgcp/public_test.go @@ -0,0 +1,464 @@ +package mgcp + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MGCPSuite) Test_PowerLimitationFactor() { + data, err := s.sut.PowerLimitationFactor(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.PowerLimitationFactor(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.DeviceConfigurationKeyValueDescriptionListDataType{ + DeviceConfigurationKeyValueDescriptionData: []model.DeviceConfigurationKeyValueDescriptionDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerLimitationFactor(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + keyData := &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(10), + }, + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeDeviceConfigurationKeyValueListData, keyData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerLimitationFactor(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MGCPSuite) Test_Power() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MGCPSuite) Test_EnergyFeedIn() { + data, err := s.sut.EnergyFeedIn(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeGridFeedIn), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MGCPSuite) Test_EnergyConsumed() { + data, err := s.sut.EnergyConsumed(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeGridConsumption), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MGCPSuite) Test_CurrentPerPhase() { + data, err := s.sut.CurrentPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} + +func (s *MGCPSuite) Test_VoltagePerPhase() { + data, err := s.sut.VoltagePerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(230), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{230, 230, 230}, data) +} + +func (s *MGCPSuite) Test_Frequency() { + data, err := s.sut.Frequency(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(50), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 50.0, data) +} diff --git a/usecases/gcp/mgcp/testhelper_test.go b/usecases/gcp/mgcp/testhelper_test.go new file mode 100644 index 00000000..b844a75d --- /dev/null +++ b/usecases/gcp/mgcp/testhelper_test.go @@ -0,0 +1,178 @@ +package mgcp + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestMGCPSuite(t *testing.T) { + suite.Run(t, new(MGCPSuite)) +} + +type MGCPSuite struct { + suite.Suite + + sut *GcpMGCP + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + smgwEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *MGCPSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *MGCPSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewGcpMGCP(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.smgwEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeGridConnectionPointOfPremises), + }, + }, + }, + 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/usecases/gcp/mgcp/types.go b/usecases/gcp/mgcp/types.go new file mode 100644 index 00000000..13952bfb --- /dev/null +++ b/usecases/gcp/mgcp/types.go @@ -0,0 +1,55 @@ +package mgcp + +import "github.com/enbility/eebus-go/api" + +const ( + // Grid maximum allowed feed-in power as percentage value of the cumulated + // nominal peak power of all electricity producting PV systems was updated + // + // Use `PowerLimitationFactor` to get the current data + // + // Use Case MGCP, Scenario 2 + DataUpdatePowerLimitationFactor api.EventType = "gcp-mgcp-DataUpdatePowerLimitationFactor" + + // Grid momentary power consumption/production data updated + // + // Use `Power` to get the current data + // + // Use Case MGCP, Scenario 2 + DataUpdatePower api.EventType = "gcp-mgcp-DataUpdatePower" + + // Total grid feed in energy data updated + // + // Use `EnergyFeedIn` to get the current data + // + // Use Case MGCP, Scenario 3 + DataUpdateEnergyFeedIn api.EventType = "gcp-mgcp-DataUpdateEnergyFeedIn" + + // Total grid consumed energy data updated + // + // Use `EnergyConsumed` to get the current data + // + // Use Case MGCP, Scenario 4 + DataUpdateEnergyConsumed api.EventType = "gcp-mgcp-DataUpdateEnergyConsumed" + + // Phase specific momentary current consumption/production phase detail data updated + // + // Use `CurrentPerPhase` to get the current data + // + // Use Case MGCP, Scenario 5 + DataUpdateCurrentPerPhase api.EventType = "gcp-mgcp-DataUpdateCurrentPerPhase" + + // Phase specific voltage at the grid connection point + // + // Use `VoltagePerPhase` to get the current data + // + // Use Case MGCP, Scenario 6 + DataUpdateVoltagePerPhase api.EventType = "gcp-mgcp-DataUpdateVoltagePerPhase" + + // Grid frequency data updated + // + // Use `Frequency` to get the current data + // + // Use Case MGCP, Scenario 7 + DataUpdateFrequency api.EventType = "gcp-mgcp-DataUpdateFrequency" +) diff --git a/usecases/gcp/mgcp/usecase.go b/usecases/gcp/mgcp/usecase.go new file mode 100644 index 00000000..61b8a594 --- /dev/null +++ b/usecases/gcp/mgcp/usecase.go @@ -0,0 +1,111 @@ +package mgcp + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + usecase "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type GcpMGCP struct { + *usecase.UseCaseBase +} + +var _ ucapi.GcpMGCPInterface = (*GcpMGCP)(nil) + +func NewGcpMGCP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *GcpMGCP { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeCEM, + model.EntityTypeTypeGridConnectionPointOfPremises, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeMonitoringAppliance, + model.UseCaseNameTypeMonitoringOfGridConnectionPoint, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5, 6, 7}, + eventCB, + validEntityTypes) + + uc := &GcpMGCP{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *GcpMGCP) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeDeviceConfiguration, + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *GcpMGCP) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeGridConnectionPoint, + e.UseCaseName, + []model.UseCaseScenarioSupportType{2, 3, 4}, + []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check if measurement description contain data for the required scope + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + } + data1, err1 := measurement.GetDescriptionsForFilter(filter) + filter.ScopeType = util.Ptr(model.ScopeTypeTypeGridFeedIn) + data2, err2 := measurement.GetDescriptionsForFilter(filter) + filter.ScopeType = util.Ptr(model.ScopeTypeTypeGridConsumption) + data3, err3 := measurement.GetDescriptionsForFilter(filter) + if err1 != nil || err2 != nil || err3 != nil || + data1 == nil || data2 == nil || data3 == nil { + return false, api.ErrDataNotAvailable + } + + // check if electrical connection descriptions is provided + electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + if _, err = electricalConnection.GetDescriptionsForFilter(model.ElectricalConnectionDescriptionDataType{}); err != nil { + return false, err + } + + return true, nil +} diff --git a/usecases/gcp/mgcp/usecase_test.go b/usecases/gcp/mgcp/usecase_test.go new file mode 100644 index 00000000..591ec368 --- /dev/null +++ b/usecases/gcp/mgcp/usecase_test.go @@ -0,0 +1,86 @@ +package mgcp + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MGCPSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *MGCPSuite) 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.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), false, data) + + ucData := &model.NodeManagementUseCaseDataType{ + UseCaseInformation: []model.UseCaseInformationDataType{ + { + Actor: util.Ptr(model.UseCaseActorTypeGridConnectionPoint), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeMonitoringOfGridConnectionPoint), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{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.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeGridFeedIn), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + ScopeType: util.Ptr(model.ScopeTypeTypeGridConsumption), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.smgwEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.smgwEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), true, data) +} diff --git a/usecases/internal/heartbeat.go b/usecases/internal/heartbeat.go new file mode 100644 index 00000000..24301e55 --- /dev/null +++ b/usecases/internal/heartbeat.go @@ -0,0 +1,22 @@ +package internal + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// IsHeartbeat checks if the given payload represents a heartbeat event. +// It returns true if the payload is a heartbeat event, and false otherwise. +func IsHeartbeat(payload spineapi.EventPayload) bool { + //revive:disable-next-line + switch payload.Data.(type) { + case *model.DeviceDiagnosisHeartbeatDataType: + return payload.Function == model.FunctionTypeDeviceDiagnosisHeartbeatData && + payload.EventType == spineapi.EventTypeDataChange && + payload.ChangeType == spineapi.ElementChangeUpdate && + payload.CmdClassifier != nil && + *payload.CmdClassifier == model.CmdClassifierTypeNotify + default: + return false + } +} diff --git a/usecases/internal/heartbeat_test.go b/usecases/internal/heartbeat_test.go new file mode 100644 index 00000000..f5e35cb4 --- /dev/null +++ b/usecases/internal/heartbeat_test.go @@ -0,0 +1,30 @@ +package internal + +import ( + "testing" + + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func Test_IsHeartbeat(t *testing.T) { + payload := spineapi.EventPayload{} + result := IsHeartbeat(payload) + assert.False(t, result) + + payload.Data = &model.DeviceDiagnosisHeartbeatDataType{} + result = IsHeartbeat(payload) + assert.False(t, result) + + payload.Function = model.FunctionTypeDeviceDiagnosisHeartbeatData + result = IsHeartbeat(payload) + assert.False(t, result) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.CmdClassifier = util.Ptr(model.CmdClassifierTypeNotify) + result = IsHeartbeat(payload) + assert.True(t, result) +} diff --git a/usecases/internal/helper.go b/usecases/internal/helper.go new file mode 100644 index 00000000..5466d40d --- /dev/null +++ b/usecases/internal/helper.go @@ -0,0 +1,47 @@ +package internal + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +var PhaseNameMapping = []model.ElectricalConnectionPhaseNameType{model.ElectricalConnectionPhaseNameTypeA, model.ElectricalConnectionPhaseNameTypeB, model.ElectricalConnectionPhaseNameTypeC} + +func IsDeviceConnected(payload spineapi.EventPayload) bool { + return payload.Device != nil && + payload.EventType == spineapi.EventTypeDeviceChange && + payload.ChangeType == spineapi.ElementChangeAdd +} + +func IsDeviceDisconnected(payload spineapi.EventPayload) bool { + return payload.Device != nil && + payload.EventType == spineapi.EventTypeDeviceChange && + payload.ChangeType == spineapi.ElementChangeRemove +} + +func IsEntityConnected(payload spineapi.EventPayload) bool { + if payload.Entity != nil && + payload.EventType == spineapi.EventTypeEntityChange && + payload.ChangeType == spineapi.ElementChangeAdd { + return true + } + + return false +} + +func IsEntityDisconnected(payload spineapi.EventPayload) bool { + if payload.Entity != nil && + payload.EventType == spineapi.EventTypeEntityChange && + payload.ChangeType == spineapi.ElementChangeRemove { + return true + } + + return false +} + +func Deref(v *string) string { + if v != nil { + return string(*v) + } + return "" +} diff --git a/usecases/internal/helper_test.go b/usecases/internal/helper_test.go new file mode 100644 index 00000000..e8f65192 --- /dev/null +++ b/usecases/internal/helper_test.go @@ -0,0 +1,65 @@ +package internal + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/mocks" + "github.com/stretchr/testify/assert" +) + +func (s *InternalSuite) Test_IsDeviceConnected() { + payload := spineapi.EventPayload{} + result := IsDeviceConnected(payload) + assert.Equal(s.T(), false, result) + + device := mocks.NewDeviceRemoteInterface(s.T()) + payload = spineapi.EventPayload{ + Device: device, + EventType: spineapi.EventTypeDeviceChange, + ChangeType: spineapi.ElementChangeAdd, + } + result = IsDeviceConnected(payload) + assert.Equal(s.T(), true, result) +} + +func (s *InternalSuite) Test_IsDeviceDisconnected() { + payload := spineapi.EventPayload{} + result := IsDeviceDisconnected(payload) + assert.Equal(s.T(), false, result) + + device := mocks.NewDeviceRemoteInterface(s.T()) + payload = spineapi.EventPayload{ + Device: device, + EventType: spineapi.EventTypeDeviceChange, + ChangeType: spineapi.ElementChangeRemove, + } + result = IsDeviceDisconnected(payload) + assert.Equal(s.T(), true, result) +} + +func (s *InternalSuite) Test_IsEntityConnected() { + payload := spineapi.EventPayload{} + result := IsEntityConnected(payload) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.evseEntity, + EventType: spineapi.EventTypeEntityChange, + ChangeType: spineapi.ElementChangeAdd, + } + result = IsEntityConnected(payload) + assert.Equal(s.T(), true, result) +} + +func (s *InternalSuite) Test_IsEntityDisconnected() { + payload := spineapi.EventPayload{} + result := IsEntityDisconnected(payload) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.evseEntity, + EventType: spineapi.EventTypeEntityChange, + ChangeType: spineapi.ElementChangeRemove, + } + result = IsEntityDisconnected(payload) + assert.Equal(s.T(), true, result) +} diff --git a/usecases/internal/loadcontrol.go b/usecases/internal/loadcontrol.go new file mode 100644 index 00000000..ee6edeba --- /dev/null +++ b/usecases/internal/loadcontrol.go @@ -0,0 +1,204 @@ +package internal + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// return the current loadcontrol limits for a categoriy +// +// possible errors: +// - ErrDataNotAvailable if no such measurement is (yet) available +// - and others +func LoadControlLimits( + localEntity spineapi.EntityLocalInterface, + remoteEntity spineapi.EntityRemoteInterface, + filter model.LoadControlLimitDescriptionDataType, +) (limits []ucapi.LoadLimitsPhase, resultErr error) { + limits = nil + resultErr = api.ErrNoCompatibleEntity + + evLoadControl, err := client.NewLoadControl(localEntity, remoteEntity) + evElectricalConnection, err2 := client.NewElectricalConnection(localEntity, remoteEntity) + if err != nil || err2 != nil { + return + } + + resultErr = api.ErrDataNotAvailable + // find out the appropriate limitId for each phase value + // limitDescription contains the measurementId for each limitId + limitDescriptions, err := evLoadControl.GetLimitDescriptionsForFilter(filter) + if err != nil || limitDescriptions == nil { + return + } + + var result []ucapi.LoadLimitsPhase + + for i := 0; i < len(PhaseNameMapping); i++ { + phaseName := PhaseNameMapping[i] + + // electricalParameterDescription contains the measured phase for each measurementId + filter := model.ElectricalConnectionParameterDescriptionDataType{ + AcMeasuredPhases: &phaseName, + } + elParamDesc, err := evElectricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(elParamDesc) == 0 || elParamDesc[0].MeasurementId == nil { + // there is no data for this phase, the phase may not exist + result = append(result, ucapi.LoadLimitsPhase{Phase: phaseName}) + continue + } + + var limitDesc *model.LoadControlLimitDescriptionDataType + for _, desc := range limitDescriptions { + if desc.MeasurementId != nil && + elParamDesc[0].MeasurementId != nil && + *desc.MeasurementId == *elParamDesc[0].MeasurementId { + safeDesc := desc + limitDesc = &safeDesc + break + } + } + + if limitDesc == nil || limitDesc.LimitId == nil { + return + } + + limitIdData, err := evLoadControl.GetLimitDataForId(*limitDesc.LimitId) + if err != nil { + return + } + + var limitValue float64 + if limitIdData.Value == nil || (limitIdData.IsLimitActive != nil && !*limitIdData.IsLimitActive) { + // report maximum possible if no limit is available or the limit is not active + filter := model.ElectricalConnectionPermittedValueSetDataType{ + ParameterId: elParamDesc[0].ParameterId, + } + _, dataMax, _, err := evElectricalConnection.GetPermittedValueDataForFilter(filter) + if err != nil { + return + } + + limitValue = dataMax + } else { + limitValue = limitIdData.Value.GetValue() + } + + newLimit := ucapi.LoadLimitsPhase{ + Phase: phaseName, + IsChangeable: (limitIdData.IsLimitChangeable != nil && *limitIdData.IsLimitChangeable), + IsActive: (limitIdData.IsLimitActive != nil && *limitIdData.IsLimitActive), + Value: limitValue, + } + + result = append(result, newLimit) + } + + return result, nil +} + +// generic helper to be used in UCOPEV & UCOSCEV +// send new LoadControlLimits to the remote EV +// +// parameters: +// - limits: a set of limits for a given limit category containing phase specific limit data +// +// category obligations: +// Sets a maximum A limit for each phase that the EV may not exceed. +// Mainly used for implementing overload protection of the site or limiting the +// maximum charge power of EVs when the EV and EVSE communicate via IEC61851 +// and with ISO15118 if the EV does not support the Optimization of Self Consumption +// usecase. +// +// category recommendations: +// Sets a recommended charge power in A for each phase. This is mainly +// used if the EV and EVSE communicate via ISO15118 to support charging excess solar power. +// The EV either needs to support the Optimization of Self Consumption usecase or +// the EVSE needs to be able map the recommendations into oligation limits which then +// works for all EVs communication either via IEC61851 or ISO15118. +// +// notes: +// - For obligations to work for optimizing solar excess power, the EV needs to have an energy demand. +// - Recommendations work even if the EV does not have an active energy demand, given it communicated with the EVSE via ISO15118 and supports the usecase. +// - In ISO15118-2 the usecase is only supported via VAS extensions which are vendor specific and needs to have specific EVSE support for the specific EV brand. +// - In ISO15118-20 this is a standard feature which does not need special support on the EVSE. +// - Min power data is only provided via IEC61851 or using VAS in ISO15118-2. +func WriteLoadControlLimits( + localEntity spineapi.EntityLocalInterface, + remoteEntity spineapi.EntityRemoteInterface, + category model.LoadControlCategoryType, + limits []ucapi.LoadLimitsPhase) (*model.MsgCounterType, error) { + loadControl, err := client.NewLoadControl(localEntity, remoteEntity) + electricalConnection, err2 := client.NewElectricalConnection(localEntity, remoteEntity) + if err != nil || err2 != nil { + return nil, api.ErrNoCompatibleEntity + } + + var limitData []model.LoadControlLimitDataType + + for _, phaseLimit := range limits { + // find out the appropriate limitId for each phase value + // limitDescription contains the measurementId for each limitId + filter := model.LoadControlLimitDescriptionDataType{ + LimitCategory: &category, + } + limitDescriptions, err := loadControl.GetLimitDescriptionsForFilter(filter) + if err != nil || limitDescriptions == nil { + continue + } + + // electricalParameterDescription contains the measured phase for each measurementId + filter2 := model.ElectricalConnectionParameterDescriptionDataType{ + AcMeasuredPhases: util.Ptr(phaseLimit.Phase), + } + elParamDesc, err := electricalConnection.GetParameterDescriptionsForFilter(filter2) + if err != nil || len(elParamDesc) == 0 || elParamDesc[0].MeasurementId == nil { + continue + } + + var limitDesc *model.LoadControlLimitDescriptionDataType + for _, desc := range limitDescriptions { + if desc.MeasurementId != nil && + elParamDesc[0].MeasurementId != nil && + *desc.MeasurementId == *elParamDesc[0].MeasurementId { + safeDesc := desc + limitDesc = &safeDesc + break + } + } + + if limitDesc == nil || limitDesc.LimitId == nil { + continue + } + + limitIdData, err := loadControl.GetLimitDataForId(*limitDesc.LimitId) + if err != nil { + continue + } + + // EEBus_UC_TS_OverloadProtectionByEvChargingCurrentCurtailment V1.01b 3.2.1.2.2.2 + // If omitted or set to "true", the timePeriod, value and isLimitActive element SHALL be writeable by a client. + if limitIdData.IsLimitChangeable != nil && !*limitIdData.IsLimitChangeable { + continue + } + + // electricalPermittedValueSet contains the allowed min, max and the default values per phase + limit := electricalConnection.AdjustValueToBeWithinPermittedValuesForParameterId( + phaseLimit.Value, *elParamDesc[0].ParameterId) + + newLimit := model.LoadControlLimitDataType{ + LimitId: limitDesc.LimitId, + IsLimitActive: util.Ptr(phaseLimit.IsActive), + Value: model.NewScaledNumberType(limit), + } + limitData = append(limitData, newLimit) + } + + msgCounter, err := loadControl.WriteLimitData(limitData) + + return msgCounter, err +} diff --git a/usecases/internal/loadcontrol_test.go b/usecases/internal/loadcontrol_test.go new file mode 100644 index 00000000..07eb069d --- /dev/null +++ b/usecases/internal/loadcontrol_test.go @@ -0,0 +1,391 @@ +package internal + +import ( + "testing" + + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *InternalSuite) Test_LoadControlLimits() { + var data []ucapi.LoadLimitsPhase + var err error + limitType := model.LoadControlLimitTypeTypeMaxValueLimit + scope := model.ScopeTypeTypeSelfConsumption + category := model.LoadControlCategoryTypeObligation + + filter := model.LoadControlLimitDescriptionDataType{ + LimitType: util.Ptr(limitType), + LimitCategory: util.Ptr(category), + ScopeType: util.Ptr(scope), + } + data, err = LoadControlLimits(nil, nil, filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = LoadControlLimits(s.localEntity, s.mockRemoteEntity, filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = LoadControlLimits(s.localEntity, s.monitoredEntity, filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: []model.LoadControlLimitDescriptionDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + LimitCategory: util.Ptr(category), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + LimitType: util.Ptr(limitType), + ScopeType: util.Ptr(scope), + }, + { + LimitId: util.Ptr(model.LoadControlLimitIdType(1)), + LimitCategory: util.Ptr(category), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + LimitType: util.Ptr(limitType), + ScopeType: util.Ptr(scope), + }, + { + LimitId: util.Ptr(model.LoadControlLimitIdType(2)), + LimitCategory: util.Ptr(category), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + LimitType: util.Ptr(limitType), + ScopeType: util.Ptr(scope), + }, + }, + } + + 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 = LoadControlLimits(s.localEntity, s.monitoredEntity, filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 3, len(data)) + assert.Equal(s.T(), 0.0, data[0].Value) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(10)), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.localEntity, s.monitoredEntity, filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + limitData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: []model.LoadControlLimitDataType{ + { + LimitId: util.Ptr(model.LoadControlLimitIdType(0)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: util.Ptr(model.LoadControlLimitIdType(1)), + Value: model.NewScaledNumberType(16), + }, + { + LimitId: util.Ptr(model.LoadControlLimitIdType(2)), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.localEntity, s.monitoredEntity, filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: []model.ElectricalConnectionPermittedValueSetDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + PermittedValueSet: []model.ScaledNumberSetType{ + { + Value: []model.ScaledNumberType{ + *model.NewScaledNumberType(0), + }, + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(6), + Max: model.NewScaledNumberType(16), + }, + }, + }, + }, + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = LoadControlLimits(s.localEntity, s.monitoredEntity, filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 3, len(data)) + assert.Equal(s.T(), 16.0, data[0].Value) +} + +func (s *InternalSuite) Test_WriteLoadControlLimits() { + loadLimits := []ucapi.LoadLimitsPhase{} + + category := model.LoadControlCategoryTypeObligation + + msgCounter, err := WriteLoadControlLimits(nil, nil, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.mockRemoteEntity, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(10)), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(1)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(2)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, loadLimits) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), msgCounter) + + type dataStruct struct { + phases int + permittedDefaultExists bool + permittedDefaultValue float64 + permittedMinValue float64 + permittedMaxValue float64 + limits, limitsExpected []float64 + } + + tests := []struct { + name string + data []dataStruct + }{ + { + "1 Phase ISO15118", + []dataStruct{ + {1, true, 0.1, 2, 16, []float64{0}, []float64{0.1}}, + {1, true, 0.1, 2, 16, []float64{2.2}, []float64{2.2}}, + {1, true, 0.1, 2, 16, []float64{10}, []float64{10}}, + {1, true, 0.1, 2, 16, []float64{16}, []float64{16}}, + }, + }, + { + "3 Phase ISO15118", + []dataStruct{ + {3, true, 0.1, 2, 16, []float64{0, 0, 0}, []float64{0.1, 0.1, 0.1}}, + {3, true, 0.1, 2, 16, []float64{2.2, 2.2, 2.2}, []float64{2.2, 2.2, 2.2}}, + {3, true, 0.1, 2, 16, []float64{10, 10, 10}, []float64{10, 10, 10}}, + {3, true, 0.1, 2, 16, []float64{16, 16, 16}, []float64{16, 16, 16}}, + }, + }, + { + "1 Phase IEC61851", + []dataStruct{ + {1, true, 0, 6, 16, []float64{0}, []float64{0}}, + {1, true, 0, 6, 16, []float64{6}, []float64{6}}, + {1, true, 0, 6, 16, []float64{10}, []float64{10}}, + {1, true, 0, 6, 16, []float64{16}, []float64{16}}, + }, + }, + { + "3 Phase IEC61851", + []dataStruct{ + {3, true, 0, 6, 16, []float64{0, 0, 0}, []float64{0, 0, 0}}, + {3, true, 0, 6, 16, []float64{6, 6, 6}, []float64{6, 6, 6}}, + {3, true, 0, 6, 16, []float64{10, 10, 10}, []float64{10, 10, 10}}, + {3, true, 0, 6, 16, []float64{16, 16, 16}, []float64{16, 16, 16}}, + }, + }, + { + "3 Phase IEC61851 Elli", + []dataStruct{ + {3, false, 0, 6, 16, []float64{0, 0, 0}, []float64{0, 0, 0}}, + {3, false, 0, 6, 16, []float64{6, 6, 6}, []float64{6, 6, 6}}, + {3, false, 0, 6, 16, []float64{10, 10, 10}, []float64{10, 10, 10}}, + {3, false, 0, 6, 16, []float64{16, 16, 16}, []float64{16, 16, 16}}, + }, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + dataSet := []model.ElectricalConnectionPermittedValueSetDataType{} + permittedData := []model.ScaledNumberSetType{} + for _, data := range tc.data { + // clean up data + remoteLoadControlF := s.monitoredEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + assert.NotNil(s.T(), remoteLoadControlF) + + emptyLimits := model.LoadControlLimitListDataType{} + errT := remoteLoadControlF.UpdateData(model.FunctionTypeLoadControlLimitListData, &emptyLimits, nil, nil) + assert.Nil(s.T(), errT) + + for phase := 0; phase < data.phases; phase++ { + item := model.ScaledNumberSetType{ + Range: []model.ScaledNumberRangeType{ + { + Min: model.NewScaledNumberType(data.permittedMinValue), + Max: model.NewScaledNumberType(data.permittedMaxValue), + }, + }, + } + if data.permittedDefaultExists { + item.Value = []model.ScaledNumberType{*model.NewScaledNumberType(data.permittedDefaultValue)} + } + permittedData = append(permittedData, item) + + permittedItem := model.ElectricalConnectionPermittedValueSetDataType{ + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + ParameterId: util.Ptr(model.ElectricalConnectionParameterIdType(phase)), + PermittedValueSet: permittedData, + } + dataSet = append(dataSet, permittedItem) + } + + permData := &model.ElectricalConnectionPermittedValueSetListDataType{ + ElectricalConnectionPermittedValueSetData: dataSet, + } + + fErr = rFeature.UpdateData(model.FunctionTypeElectricalConnectionPermittedValueSetListData, permData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err := WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, loadLimits) + assert.NotNil(t, err) + assert.Nil(t, msgCounter) + + limitDesc := []model.LoadControlLimitDescriptionDataType{} + for index := range data.limits { + id := model.LoadControlLimitIdType(index) + limitItem := model.LoadControlLimitDescriptionDataType{ + LimitId: util.Ptr(id), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeObligation), + MeasurementId: util.Ptr(model.MeasurementIdType(index)), + } + limitDesc = append(limitDesc, limitItem) + } + add := len(limitDesc) + for index := range data.limits { + id := model.LoadControlLimitIdType(index + add) + limitItem := model.LoadControlLimitDescriptionDataType{ + LimitId: util.Ptr(id), + LimitCategory: util.Ptr(model.LoadControlCategoryTypeRecommendation), + MeasurementId: util.Ptr(model.MeasurementIdType(index)), + } + limitDesc = append(limitDesc, limitItem) + } + + descData := &model.LoadControlLimitDescriptionListDataType{ + LoadControlLimitDescriptionData: limitDesc, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeServer) + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, loadLimits) + assert.NotNil(t, err) + assert.Nil(t, msgCounter) + + limitData := []model.LoadControlLimitDataType{} + for index := range limitDesc { + limitItem := model.LoadControlLimitDataType{ + LimitId: util.Ptr(model.LoadControlLimitIdType(index)), + IsLimitChangeable: util.Ptr(true), + IsLimitActive: util.Ptr(false), + Value: model.NewScaledNumberType(data.permittedMaxValue), + } + limitData = append(limitData, limitItem) + } + + limitListData := &model.LoadControlLimitListDataType{ + LoadControlLimitData: limitData, + } + + fErr = rFeature.UpdateData(model.FunctionTypeLoadControlLimitListData, limitListData, nil, nil) + assert.Nil(s.T(), fErr) + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, loadLimits) + assert.NotNil(t, err) + assert.Nil(t, msgCounter) + + phaseLimitValues := []ucapi.LoadLimitsPhase{} + for index, limit := range data.limits { + phase := PhaseNameMapping[index] + phaseLimitValues = append(phaseLimitValues, ucapi.LoadLimitsPhase{ + Phase: phase, + IsActive: true, + Value: limit, + }) + } + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, phaseLimitValues) + assert.Nil(t, err) + assert.NotNil(t, msgCounter) + + msgCounter, err = WriteLoadControlLimits(s.localEntity, s.monitoredEntity, category, phaseLimitValues) + assert.Nil(t, err) + assert.NotNil(t, msgCounter) + } + }) + } +} diff --git a/usecases/internal/manufacturerdata.go b/usecases/internal/manufacturerdata.go new file mode 100644 index 00000000..d44f380d --- /dev/null +++ b/usecases/internal/manufacturerdata.go @@ -0,0 +1,41 @@ +package internal + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" +) + +// return the current manufacturer data for a entity +// +// possible errors: +// - ErrNoCompatibleEntity if entity is not compatible +// - and others +func ManufacturerData(localEntity spineapi.EntityLocalInterface, entity spineapi.EntityRemoteInterface) (api.ManufacturerData, error) { + deviceClassification, err := client.NewDeviceClassification(localEntity, entity) + if err != nil { + return api.ManufacturerData{}, err + } + + data, err := deviceClassification.GetManufacturerDetails() + if err != nil { + return api.ManufacturerData{}, err + } + + ret := api.ManufacturerData{ + DeviceName: Deref((*string)(data.DeviceName)), + DeviceCode: Deref((*string)(data.DeviceCode)), + SerialNumber: Deref((*string)(data.SerialNumber)), + SoftwareRevision: Deref((*string)(data.SoftwareRevision)), + HardwareRevision: Deref((*string)(data.HardwareRevision)), + VendorName: Deref((*string)(data.VendorName)), + VendorCode: Deref((*string)(data.VendorCode)), + BrandName: Deref((*string)(data.BrandName)), + PowerSource: Deref((*string)(data.PowerSource)), + ManufacturerNodeIdentification: Deref((*string)(data.ManufacturerNodeIdentification)), + ManufacturerLabel: Deref((*string)(data.ManufacturerLabel)), + ManufacturerDescription: Deref((*string)(data.ManufacturerDescription)), + } + + return ret, nil +} diff --git a/usecases/internal/manufacturerdata_test.go b/usecases/internal/manufacturerdata_test.go new file mode 100644 index 00000000..4afc7963 --- /dev/null +++ b/usecases/internal/manufacturerdata_test.go @@ -0,0 +1,37 @@ +package internal + +import ( + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *InternalSuite) Test_ManufacturerData() { + _, err := ManufacturerData(nil, nil) + assert.NotNil(s.T(), err) + + _, err = ManufacturerData(s.localEntity, s.mockRemoteEntity) + assert.NotNil(s.T(), err) + + _, err = ManufacturerData(s.localEntity, s.monitoredEntity) + assert.NotNil(s.T(), err) + + descData := &model.DeviceClassificationManufacturerDataType{ + + DeviceName: util.Ptr(model.DeviceClassificationStringType("deviceName")), + DeviceCode: util.Ptr(model.DeviceClassificationStringType("deviceCode")), + SerialNumber: util.Ptr(model.DeviceClassificationStringType("serialNumber")), + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + assert.NotNil(s.T(), rFeature) + fErr := rFeature.UpdateData(model.FunctionTypeDeviceClassificationManufacturerData, descData, nil, nil) + assert.Nil(s.T(), fErr) + data, err := ManufacturerData(s.localEntity, s.monitoredEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Equal(s.T(), "deviceName", data.DeviceName) + assert.Equal(s.T(), "deviceCode", data.DeviceCode) + assert.Equal(s.T(), "serialNumber", data.SerialNumber) + assert.Equal(s.T(), "", data.SoftwareRevision) +} diff --git a/usecases/internal/measurement.go b/usecases/internal/measurement.go new file mode 100644 index 00000000..21b73484 --- /dev/null +++ b/usecases/internal/measurement.go @@ -0,0 +1,71 @@ +package internal + +import ( + "slices" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// return the phase specific measurement data +func MeasurementPhaseSpecificDataForFilter( + localEntity spineapi.EntityLocalInterface, + remoteEntity spineapi.EntityRemoteInterface, + measurementFilter model.MeasurementDescriptionDataType, + energyDirection model.EnergyDirectionType, + validPhaseNameTypes []model.ElectricalConnectionPhaseNameType, +) ([]float64, error) { + measurement, err := client.NewMeasurement(localEntity, remoteEntity) + electricalConnection, err1 := client.NewElectricalConnection(localEntity, remoteEntity) + if err != nil || err1 != nil { + return nil, api.ErrMetadataNotAvailable + } + + data, err := measurement.GetDataForFilter(measurementFilter) + if err != nil || len(data) == 0 { + return nil, api.ErrDataNotAvailable + } + + var result []float64 + + for _, item := range data { + if item.Value == nil || item.MeasurementId == nil { + continue + } + + if validPhaseNameTypes != nil { + filter := model.ElectricalConnectionParameterDescriptionDataType{ + MeasurementId: item.MeasurementId, + } + param, err := electricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(param) == 0 || + param[0].AcMeasuredPhases == nil || + !slices.Contains(validPhaseNameTypes, *param[0].AcMeasuredPhases) { + continue + } + } + + if energyDirection != "" { + filter := model.ElectricalConnectionParameterDescriptionDataType{ + MeasurementId: item.MeasurementId, + } + desc, err := electricalConnection.GetDescriptionForParameterDescriptionFilter(filter) + if err != nil || desc == nil { + continue + } + + // if energy direction is not consume + if desc.PositiveEnergyDirection == nil || *desc.PositiveEnergyDirection != energyDirection { + return nil, err + } + } + + value := item.Value.GetValue() + + result = append(result, value) + } + + return result, nil +} diff --git a/usecases/internal/measurement_test.go b/usecases/internal/measurement_test.go new file mode 100644 index 00000000..b2e25eb0 --- /dev/null +++ b/usecases/internal/measurement_test.go @@ -0,0 +1,163 @@ +package internal + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *InternalSuite) Test_MeasurementPhaseSpecificDataForFilter() { + measurementType := model.MeasurementTypeTypePower + commodityType := model.CommodityTypeTypeElectricity + scopeType := model.ScopeTypeTypeACPower + energyDirection := model.EnergyDirectionTypeConsume + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: &measurementType, + CommodityType: &commodityType, + ScopeType: &scopeType, + } + + data, err := MeasurementPhaseSpecificDataForFilter(nil, nil, filter, energyDirection, PhaseNameMapping) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.mockRemoteEntity, + filter, + energyDirection, + PhaseNameMapping, + ) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + PhaseNameMapping, + ) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + PhaseNameMapping, + ) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(10)), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + PhaseNameMapping, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + PhaseNameMapping, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} diff --git a/usecases/internal/testhelper_test.go b/usecases/internal/testhelper_test.go new file mode 100644 index 00000000..34dd82d8 --- /dev/null +++ b/usecases/internal/testhelper_test.go @@ -0,0 +1,229 @@ +package internal + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestInternalSuite(t *testing.T) { + suite.Run(t, new(InternalSuite)) +} + +type InternalSuite struct { + suite.Suite + + service api.ServiceInterface + + localEntity spineapi.EntityLocalInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evseEntity spineapi.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *InternalSuite) Event(ski string, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *InternalSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + + var entities []spineapi.EntityRemoteInterface + + s.localEntity, s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evseEntity = entities[0] + s.monitoredEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.EntityLocalInterface, + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + f := spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(3, localEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(4, localEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(5, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeClient) + localEntity.AddFeature(f) + 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) + f = spine.NewFeatureLocal(3, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(4, localEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceClassificationManufacturerData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceClassificationUserData, 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) + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeMeasurement, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeDeviceClassification, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + model.FunctionTypeDeviceClassificationUserData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + + remoteDeviceName := "remote" + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return localEntity, remoteDevice, entities +} diff --git a/usecases/ma/mpc/events.go b/usecases/ma/mpc/events.go new file mode 100644 index 00000000..b37f5713 --- /dev/null +++ b/usecases/ma/mpc/events.go @@ -0,0 +1,126 @@ +package mpc + +import ( + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// handle SPINE events +func (e *MaMPC) HandleEvent(payload spineapi.EventPayload) { + // only about events from an SGMW entity or device changes for this remote device + + if !e.IsCompatibleEntity(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.deviceConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.MeasurementDescriptionListDataType: + e.deviceMeasurementDescriptionDataUpdate(payload.Entity) + case *model.MeasurementListDataType: + e.deviceMeasurementDataUpdate(payload) + } +} + +// process required steps when a device is connected +func (e *MaMPC) deviceConnected(entity spineapi.EntityRemoteInterface) { + if electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity); err == nil { + if _, err := electricalConnection.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get electrical connection parameter + if _, err := electricalConnection.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := electricalConnection.RequestParameterDescriptions(); err != nil { + logging.Log().Error(err) + } + } + + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + if _, err := measurement.Subscribe(); err != nil { + logging.Log().Error(err) + } + + // get measurement parameters + if _, err := measurement.RequestDescriptions(); err != nil { + logging.Log().Error(err) + } + + if _, err := measurement.RequestConstraints(); err != nil { + logging.Log().Error(err) + } + } +} + +// the measurement descriptiondata of a device was updated +func (e *MaMPC) deviceMeasurementDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if measurement, err := client.NewMeasurement(e.LocalEntity, entity); err == nil { + // measurement descriptions received, now get the data + if _, err := measurement.RequestData(); err != nil { + logging.Log().Error("Error getting measurement list values:", err) + } + } +} + +// the measurement data of a device was updated +func (e *MaMPC) deviceMeasurementDataUpdate(payload spineapi.EventPayload) { + if measurement, err := client.NewMeasurement(e.LocalEntity, payload.Entity); err == nil { + // Scenario 1 + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePower) + } + + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACPower) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdatePowerPerPhase) + } + + // Scenario 2 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACEnergyConsumed) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyConsumed) + } + + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACEnergyProduced) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateEnergyProduced) + } + + // Scenario 3 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACCurrent) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateCurrentsPerPhase) + } + + // Scenario 4 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACVoltage) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateVoltagePerPhase) + } + + // Scenario 5 + filter.ScopeType = util.Ptr(model.ScopeTypeTypeACFrequency) + if measurement.CheckEventPayloadDataForFilter(payload.Data, filter) { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateFrequency) + } + } +} diff --git a/usecases/ma/mpc/events_test.go b/usecases/ma/mpc/events_test.go new file mode 100644 index 00000000..99b32793 --- /dev/null +++ b/usecases/ma/mpc/events_test.go @@ -0,0 +1,128 @@ +package mpc + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MPCSuite) 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 = util.Ptr(model.MeasurementDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.MeasurementListDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *MPCSuite) Test_Failures() { + s.sut.deviceConnected(s.mockRemoteEntity) + + s.sut.deviceMeasurementDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *MPCSuite) Test_deviceMeasurementDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Device: s.remoteDevice, + Entity: s.monitoredEntity, + } + s.sut.deviceMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyConsumed), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(3)), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyProduced), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(4)), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(5)), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(6)), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + s.sut.deviceMeasurementDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + data := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(3)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(4)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(5)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(6)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + payload.Data = data + + s.sut.deviceMeasurementDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/ma/mpc/public.go b/usecases/ma/mpc/public.go new file mode 100644 index 00000000..c57425f1 --- /dev/null +++ b/usecases/ma/mpc/public.go @@ -0,0 +1,185 @@ +package mpc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + internal "github.com/enbility/eebus-go/usecases/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +// Scenario 1 + +// return the momentary active power consumption or production +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *MaMPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + values, err := internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, nil) + if err != nil { + return 0, err + } + if len(values) != 1 { + return 0, api.ErrDataNotAvailable + } + return values[0], nil +} + +// return the momentary active phase specific power consumption or production per phase +// +// possible errors: +// - ErrDataNotAvailable if no such limit is (yet) available +// - and others +func (e *MaMPC) PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + } + return internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, internal.PhaseNameMapping) +} + +// Scenario 2 + +// return the total consumption energy +// +// - positive values are used for consumption +func (e *MaMPC) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyConsumed), + } + values, err := measurement.GetDataForFilter(filter) + if err != nil || len(values) == 0 { + return 0, api.ErrDataNotAvailable + } + + // we assume thre is only one result + value := values[0].Value + if value == nil { + return 0, api.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// return the total feed in energy +// +// - negative values are used for production +func (e *MaMPC) EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyProduced), + } + values, err := measurement.GetDataForFilter(filter) + if err != nil || len(values) == 0 { + return 0, api.ErrDataNotAvailable + } + + // we assume thre is only one result + value := values[0].Value + if value == nil { + return 0, api.ErrDataNotAvailable + } + + return value.GetValue(), nil +} + +// Scenario 3 + +// return the momentary phase specific current consumption or production +// +// - positive values are used for consumption +// - negative values are used for production +func (e *MaMPC) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + } + return internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, internal.PhaseNameMapping) +} + +// Scenario 4 + +// return the phase specific voltage details +func (e *MaMPC) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { + if !e.IsCompatibleEntity(entity) { + return nil, api.ErrNoCompatibleEntity + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + } + return internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, "", internal.PhaseNameMapping) +} + +// Scenario 5 + +// return frequency +func (e *MaMPC) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) { + if !e.IsCompatibleEntity(entity) { + return 0, api.ErrNoCompatibleEntity + } + + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return 0, err + } + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + } + data, err := measurement.GetDataForFilter(filter) + if err != nil || len(data) == 0 || data[0].Value == nil { + return 0, api.ErrDataNotAvailable + } + + // take the first item + value := data[0].Value + + return value.GetValue(), nil +} diff --git a/usecases/ma/mpc/public_test.go b/usecases/ma/mpc/public_test.go new file mode 100644 index 00000000..1dad6be2 --- /dev/null +++ b/usecases/ma/mpc/public_test.go @@ -0,0 +1,523 @@ +package mpc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MPCSuite) Test_Power() { + data, err := s.sut.Power(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Power(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MPCSuite) Test_PowerPerPhase() { + data, err := s.sut.PowerPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.PowerPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} + +func (s *MPCSuite) Test_EnergyConsumed() { + data, err := s.sut.EnergyConsumed(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyConsumed), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MPCSuite) Test_EnergyProduced() { + data, err := s.sut.EnergyProduced(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyProduced), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 10.0, data) +} + +func (s *MPCSuite) Test_CurrentPerPhase() { + data, err := s.sut.CurrentPerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.CurrentPerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{10, 10, 10}, data) +} + +func (s *MPCSuite) Test_VoltagePerPhase() { + data, err := s.sut.VoltagePerPhase(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(230), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + Value: model.NewScaledNumberType(230), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 0, len(data)) + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeC), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = rElFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.VoltagePerPhase(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{230, 230, 230}, data) +} + +func (s *MPCSuite) Test_Frequency() { + data, err := s.sut.Frequency(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypeFrequency), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACFrequency), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr := rFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(50), + }, + }, + } + + fErr = rFeature.UpdateData(model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 50.0, data) +} diff --git a/usecases/ma/mpc/testhelper_test.go b/usecases/ma/mpc/testhelper_test.go new file mode 100644 index 00000000..c504d4bd --- /dev/null +++ b/usecases/ma/mpc/testhelper_test.go @@ -0,0 +1,172 @@ +package mpc + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestMPCSuite(t *testing.T) { + suite.Run(t, new(MPCSuite)) +} + +type MPCSuite struct { + suite.Suite + + sut *MaMPC + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +func (s *MPCSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *MPCSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewMaMPC(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.monitoredEntity = setupDevices(s.service, s.T()) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeMeasurement, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementConstraintsListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionDescriptionListData, + }, + }, + } + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.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/usecases/ma/mpc/types.go b/usecases/ma/mpc/types.go new file mode 100644 index 00000000..a73442d7 --- /dev/null +++ b/usecases/ma/mpc/types.go @@ -0,0 +1,54 @@ +package mpc + +import "github.com/enbility/eebus-go/api" + +const ( + // Total momentary active power consumption or production + // + // Use `Power` to get the current data + // + // Use Case MCP, Scenario 1 + DataUpdatePower api.EventType = "ma-mpc-DataUpdatePower" + + // Phase specific momentary active power consumption or production + // + // Use `PowerPerPhase` to get the current data + // + // Use Case MCP, Scenario 1 + DataUpdatePowerPerPhase api.EventType = "ma-mpc-DataUpdatePowerPerPhase" + + // Total energy consumed + // + // Use `EnergyConsumed` to get the current data + // + // Use Case MCP, Scenario 2 + DataUpdateEnergyConsumed api.EventType = "ma-mpc-DataUpdateEnergyConsumed" + + // Total energy produced + // + // Use `EnergyProduced` to get the current data + // + // Use Case MCP, Scenario 2 + DataUpdateEnergyProduced api.EventType = "ma-mpc-DataUpdateEnergyProduced" + + // Phase specific momentary current consumption or production + // + // Use `CurrentPerPhase` to get the current data + // + // Use Case MCP, Scenario 3 + DataUpdateCurrentsPerPhase api.EventType = "ma-mpc-DataUpdateCurrentsPerPhase" + + // Phase specific voltage + // + // Use `VoltagePerPhase` to get the current data + // + // Use Case MCP, Scenario 3 + DataUpdateVoltagePerPhase api.EventType = "ma-mpc-DataUpdateVoltagePerPhase" + + // Power network frequency data updated + // + // Use `Frequency` to get the current data + // + // Use Case MCP, Scenario 3 + DataUpdateFrequency api.EventType = "ma-mpc-DataUpdateFrequency" +) diff --git a/usecases/ma/mpc/usecase.go b/usecases/ma/mpc/usecase.go new file mode 100644 index 00000000..df8b1f9f --- /dev/null +++ b/usecases/ma/mpc/usecase.go @@ -0,0 +1,113 @@ +package mpc + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + ucapi "github.com/enbility/eebus-go/usecases/api" + usecase "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" +) + +type MaMPC struct { + *usecase.UseCaseBase +} + +var _ ucapi.MaMPCInterface = (*MaMPC)(nil) + +func NewMaMPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *MaMPC { + validEntityTypes := []model.EntityTypeType{ + model.EntityTypeTypeCompressor, + model.EntityTypeTypeElectricalImmersionHeater, + model.EntityTypeTypeEVSE, + model.EntityTypeTypeHeatPumpAppliance, + model.EntityTypeTypeInverter, + model.EntityTypeTypeSmartEnergyAppliance, + model.EntityTypeTypeSubMeterElectricity, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeMonitoringAppliance, + model.UseCaseNameTypeMonitoringOfPowerConsumption, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1, 2, 3, 4, 5}, + eventCB, + validEntityTypes) + + uc := &MaMPC{ + UseCaseBase: usecase, + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +func (e *MaMPC) AddFeatures() { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + } + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} + +// returns if the entity supports the usecase +// +// possible errors: +// - ErrDataNotAvailable if that information is not (yet) available +// - and others +func (e *MaMPC) IsUseCaseSupported(entity spineapi.EntityRemoteInterface) (bool, error) { + if !e.IsCompatibleEntity(entity) { + 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.UseCaseActorTypeMonitoredUnit, + e.UseCaseName, + []model.UseCaseScenarioSupportType{1}, + []model.FeatureTypeType{ + model.FeatureTypeTypeElectricalConnection, + model.FeatureTypeTypeMeasurement, + }, + ) { + return false, nil + } + + // check if measurement description contain data for the required scope + measurement, err := client.NewMeasurement(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + filter := model.MeasurementDescriptionDataType{ + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + } + if data, err := measurement.GetDescriptionsForFilter(filter); data == nil || err != nil { + return false, api.ErrDataNotAvailable + } + + // check if electrical connection descriptions is provided + electricalConnection, err := client.NewElectricalConnection(e.LocalEntity, entity) + if err != nil { + return false, api.ErrFunctionNotSupported + } + + if _, err = electricalConnection.GetDescriptionsForFilter(model.ElectricalConnectionDescriptionDataType{}); err != nil { + return false, err + } + + if _, err = electricalConnection.GetParameterDescriptionsForFilter(model.ElectricalConnectionParameterDescriptionDataType{}); err != nil { + return false, err + } + + return true, nil +} diff --git a/usecases/ma/mpc/usecase_test.go b/usecases/ma/mpc/usecase_test.go new file mode 100644 index 00000000..682f9979 --- /dev/null +++ b/usecases/ma/mpc/usecase_test.go @@ -0,0 +1,93 @@ +package mpc + +import ( + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" +) + +func (s *MPCSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} + +func (s *MPCSuite) 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: util.Ptr(model.UseCaseActorTypeMonitoredUnit), + UseCaseSupport: []model.UseCaseSupportType{ + { + UseCaseName: util.Ptr(model.UseCaseNameTypeMonitoringOfPowerConsumption), + UseCaseAvailable: util.Ptr(true), + ScenarioSupport: []model.UseCaseScenarioSupportType{1}, + }, + }, + }, + }, + } + + 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.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), + }, + }, + } + + measurementFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + fErr = measurementFeature.UpdateData(model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + elData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + elFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionDescriptionListData, elData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.IsUseCaseSupported(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), false, data) + + paramData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + }, + }, + } + + fErr = elFeature.UpdateData(model.FunctionTypeElectricalConnectionParameterDescriptionListData, paramData, 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/usecases/mocks/CemCEVCInterface.go b/usecases/mocks/CemCEVCInterface.go new file mode 100644 index 00000000..6f0ddb25 --- /dev/null +++ b/usecases/mocks/CemCEVCInterface.go @@ -0,0 +1,705 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + spine_goapi "github.com/enbility/spine-go/api" +) + +// CemCEVCInterface is an autogenerated mock type for the CemCEVCInterface type +type CemCEVCInterface struct { + mock.Mock +} + +type CemCEVCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemCEVCInterface) EXPECT() *CemCEVCInterface_Expecter { + return &CemCEVCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemCEVCInterface) AddFeatures() { + _m.Called() +} + +// CemCEVCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemCEVCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemCEVCInterface_Expecter) AddFeatures() *CemCEVCInterface_AddFeatures_Call { + return &CemCEVCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemCEVCInterface_AddFeatures_Call) Run(run func()) *CemCEVCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemCEVCInterface_AddFeatures_Call) Return() *CemCEVCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemCEVCInterface_AddFeatures_Call) RunAndReturn(run func()) *CemCEVCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemCEVCInterface) AddUseCase() { + _m.Called() +} + +// CemCEVCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemCEVCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemCEVCInterface_Expecter) AddUseCase() *CemCEVCInterface_AddUseCase_Call { + return &CemCEVCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemCEVCInterface_AddUseCase_Call) Run(run func()) *CemCEVCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemCEVCInterface_AddUseCase_Call) Return() *CemCEVCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemCEVCInterface_AddUseCase_Call) RunAndReturn(run func()) *CemCEVCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ChargePlan provides a mock function with given fields: entity +func (_m *CemCEVCInterface) ChargePlan(entity spine_goapi.EntityRemoteInterface) (api.ChargePlan, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargePlan") + } + + var r0 api.ChargePlan + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.ChargePlan, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.ChargePlan); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(api.ChargePlan) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemCEVCInterface_ChargePlan_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargePlan' +type CemCEVCInterface_ChargePlan_Call struct { + *mock.Call +} + +// ChargePlan is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) ChargePlan(entity interface{}) *CemCEVCInterface_ChargePlan_Call { + return &CemCEVCInterface_ChargePlan_Call{Call: _e.mock.On("ChargePlan", entity)} +} + +func (_c *CemCEVCInterface_ChargePlan_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_ChargePlan_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_ChargePlan_Call) Return(_a0 api.ChargePlan, _a1 error) *CemCEVCInterface_ChargePlan_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemCEVCInterface_ChargePlan_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.ChargePlan, error)) *CemCEVCInterface_ChargePlan_Call { + _c.Call.Return(run) + return _c +} + +// ChargePlanConstraints provides a mock function with given fields: entity +func (_m *CemCEVCInterface) ChargePlanConstraints(entity spine_goapi.EntityRemoteInterface) ([]api.DurationSlotValue, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargePlanConstraints") + } + + var r0 []api.DurationSlotValue + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]api.DurationSlotValue, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []api.DurationSlotValue); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api.DurationSlotValue) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemCEVCInterface_ChargePlanConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargePlanConstraints' +type CemCEVCInterface_ChargePlanConstraints_Call struct { + *mock.Call +} + +// ChargePlanConstraints is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) ChargePlanConstraints(entity interface{}) *CemCEVCInterface_ChargePlanConstraints_Call { + return &CemCEVCInterface_ChargePlanConstraints_Call{Call: _e.mock.On("ChargePlanConstraints", entity)} +} + +func (_c *CemCEVCInterface_ChargePlanConstraints_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_ChargePlanConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_ChargePlanConstraints_Call) Return(_a0 []api.DurationSlotValue, _a1 error) *CemCEVCInterface_ChargePlanConstraints_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemCEVCInterface_ChargePlanConstraints_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]api.DurationSlotValue, error)) *CemCEVCInterface_ChargePlanConstraints_Call { + _c.Call.Return(run) + return _c +} + +// ChargeStrategy provides a mock function with given fields: remoteEntity +func (_m *CemCEVCInterface) ChargeStrategy(remoteEntity spine_goapi.EntityRemoteInterface) api.EVChargeStrategyType { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for ChargeStrategy") + } + + var r0 api.EVChargeStrategyType + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.EVChargeStrategyType); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(api.EVChargeStrategyType) + } + + return r0 +} + +// CemCEVCInterface_ChargeStrategy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargeStrategy' +type CemCEVCInterface_ChargeStrategy_Call struct { + *mock.Call +} + +// ChargeStrategy is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) ChargeStrategy(remoteEntity interface{}) *CemCEVCInterface_ChargeStrategy_Call { + return &CemCEVCInterface_ChargeStrategy_Call{Call: _e.mock.On("ChargeStrategy", remoteEntity)} +} + +func (_c *CemCEVCInterface_ChargeStrategy_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_ChargeStrategy_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_ChargeStrategy_Call) Return(_a0 api.EVChargeStrategyType) *CemCEVCInterface_ChargeStrategy_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemCEVCInterface_ChargeStrategy_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) api.EVChargeStrategyType) *CemCEVCInterface_ChargeStrategy_Call { + _c.Call.Return(run) + return _c +} + +// EnergyDemand provides a mock function with given fields: remoteEntity +func (_m *CemCEVCInterface) EnergyDemand(remoteEntity spine_goapi.EntityRemoteInterface) (api.Demand, error) { + ret := _m.Called(remoteEntity) + + if len(ret) == 0 { + panic("no return value specified for EnergyDemand") + } + + var r0 api.Demand + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.Demand, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.Demand); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(api.Demand) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemCEVCInterface_EnergyDemand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyDemand' +type CemCEVCInterface_EnergyDemand_Call struct { + *mock.Call +} + +// EnergyDemand is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) EnergyDemand(remoteEntity interface{}) *CemCEVCInterface_EnergyDemand_Call { + return &CemCEVCInterface_EnergyDemand_Call{Call: _e.mock.On("EnergyDemand", remoteEntity)} +} + +func (_c *CemCEVCInterface_EnergyDemand_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_EnergyDemand_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_EnergyDemand_Call) Return(_a0 api.Demand, _a1 error) *CemCEVCInterface_EnergyDemand_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemCEVCInterface_EnergyDemand_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.Demand, error)) *CemCEVCInterface_EnergyDemand_Call { + _c.Call.Return(run) + return _c +} + +// IncentiveConstraints provides a mock function with given fields: entity +func (_m *CemCEVCInterface) IncentiveConstraints(entity spine_goapi.EntityRemoteInterface) (api.IncentiveSlotConstraints, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IncentiveConstraints") + } + + var r0 api.IncentiveSlotConstraints + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.IncentiveSlotConstraints, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.IncentiveSlotConstraints); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(api.IncentiveSlotConstraints) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemCEVCInterface_IncentiveConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IncentiveConstraints' +type CemCEVCInterface_IncentiveConstraints_Call struct { + *mock.Call +} + +// IncentiveConstraints is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) IncentiveConstraints(entity interface{}) *CemCEVCInterface_IncentiveConstraints_Call { + return &CemCEVCInterface_IncentiveConstraints_Call{Call: _e.mock.On("IncentiveConstraints", entity)} +} + +func (_c *CemCEVCInterface_IncentiveConstraints_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_IncentiveConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_IncentiveConstraints_Call) Return(_a0 api.IncentiveSlotConstraints, _a1 error) *CemCEVCInterface_IncentiveConstraints_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemCEVCInterface_IncentiveConstraints_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.IncentiveSlotConstraints, error)) *CemCEVCInterface_IncentiveConstraints_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemCEVCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemCEVCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemCEVCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemCEVCInterface_IsCompatibleEntity_Call { + return &CemCEVCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemCEVCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemCEVCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemCEVCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemCEVCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemCEVCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemCEVCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemCEVCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemCEVCInterface_IsUseCaseSupported_Call { + return &CemCEVCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemCEVCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemCEVCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemCEVCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemCEVCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// TimeSlotConstraints provides a mock function with given fields: entity +func (_m *CemCEVCInterface) TimeSlotConstraints(entity spine_goapi.EntityRemoteInterface) (api.TimeSlotConstraints, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for TimeSlotConstraints") + } + + var r0 api.TimeSlotConstraints + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.TimeSlotConstraints, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.TimeSlotConstraints); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(api.TimeSlotConstraints) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemCEVCInterface_TimeSlotConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TimeSlotConstraints' +type CemCEVCInterface_TimeSlotConstraints_Call struct { + *mock.Call +} + +// TimeSlotConstraints is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemCEVCInterface_Expecter) TimeSlotConstraints(entity interface{}) *CemCEVCInterface_TimeSlotConstraints_Call { + return &CemCEVCInterface_TimeSlotConstraints_Call{Call: _e.mock.On("TimeSlotConstraints", entity)} +} + +func (_c *CemCEVCInterface_TimeSlotConstraints_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemCEVCInterface_TimeSlotConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemCEVCInterface_TimeSlotConstraints_Call) Return(_a0 api.TimeSlotConstraints, _a1 error) *CemCEVCInterface_TimeSlotConstraints_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemCEVCInterface_TimeSlotConstraints_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.TimeSlotConstraints, error)) *CemCEVCInterface_TimeSlotConstraints_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemCEVCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemCEVCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemCEVCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemCEVCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemCEVCInterface_UpdateUseCaseAvailability_Call { + return &CemCEVCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemCEVCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemCEVCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemCEVCInterface_UpdateUseCaseAvailability_Call) Return() *CemCEVCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemCEVCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemCEVCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// WriteIncentiveTableDescriptions provides a mock function with given fields: entity, data +func (_m *CemCEVCInterface) WriteIncentiveTableDescriptions(entity spine_goapi.EntityRemoteInterface, data []api.IncentiveTariffDescription) error { + ret := _m.Called(entity, data) + + if len(ret) == 0 { + panic("no return value specified for WriteIncentiveTableDescriptions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.IncentiveTariffDescription) error); ok { + r0 = rf(entity, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CemCEVCInterface_WriteIncentiveTableDescriptions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteIncentiveTableDescriptions' +type CemCEVCInterface_WriteIncentiveTableDescriptions_Call struct { + *mock.Call +} + +// WriteIncentiveTableDescriptions is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - data []api.IncentiveTariffDescription +func (_e *CemCEVCInterface_Expecter) WriteIncentiveTableDescriptions(entity interface{}, data interface{}) *CemCEVCInterface_WriteIncentiveTableDescriptions_Call { + return &CemCEVCInterface_WriteIncentiveTableDescriptions_Call{Call: _e.mock.On("WriteIncentiveTableDescriptions", entity, data)} +} + +func (_c *CemCEVCInterface_WriteIncentiveTableDescriptions_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, data []api.IncentiveTariffDescription)) *CemCEVCInterface_WriteIncentiveTableDescriptions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].([]api.IncentiveTariffDescription)) + }) + return _c +} + +func (_c *CemCEVCInterface_WriteIncentiveTableDescriptions_Call) Return(_a0 error) *CemCEVCInterface_WriteIncentiveTableDescriptions_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemCEVCInterface_WriteIncentiveTableDescriptions_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, []api.IncentiveTariffDescription) error) *CemCEVCInterface_WriteIncentiveTableDescriptions_Call { + _c.Call.Return(run) + return _c +} + +// WriteIncentives provides a mock function with given fields: entity, data +func (_m *CemCEVCInterface) WriteIncentives(entity spine_goapi.EntityRemoteInterface, data []api.DurationSlotValue) error { + ret := _m.Called(entity, data) + + if len(ret) == 0 { + panic("no return value specified for WriteIncentives") + } + + var r0 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.DurationSlotValue) error); ok { + r0 = rf(entity, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CemCEVCInterface_WriteIncentives_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteIncentives' +type CemCEVCInterface_WriteIncentives_Call struct { + *mock.Call +} + +// WriteIncentives is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - data []api.DurationSlotValue +func (_e *CemCEVCInterface_Expecter) WriteIncentives(entity interface{}, data interface{}) *CemCEVCInterface_WriteIncentives_Call { + return &CemCEVCInterface_WriteIncentives_Call{Call: _e.mock.On("WriteIncentives", entity, data)} +} + +func (_c *CemCEVCInterface_WriteIncentives_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, data []api.DurationSlotValue)) *CemCEVCInterface_WriteIncentives_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].([]api.DurationSlotValue)) + }) + return _c +} + +func (_c *CemCEVCInterface_WriteIncentives_Call) Return(_a0 error) *CemCEVCInterface_WriteIncentives_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemCEVCInterface_WriteIncentives_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, []api.DurationSlotValue) error) *CemCEVCInterface_WriteIncentives_Call { + _c.Call.Return(run) + return _c +} + +// WritePowerLimits provides a mock function with given fields: entity, data +func (_m *CemCEVCInterface) WritePowerLimits(entity spine_goapi.EntityRemoteInterface, data []api.DurationSlotValue) error { + ret := _m.Called(entity, data) + + if len(ret) == 0 { + panic("no return value specified for WritePowerLimits") + } + + var r0 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.DurationSlotValue) error); ok { + r0 = rf(entity, data) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CemCEVCInterface_WritePowerLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WritePowerLimits' +type CemCEVCInterface_WritePowerLimits_Call struct { + *mock.Call +} + +// WritePowerLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - data []api.DurationSlotValue +func (_e *CemCEVCInterface_Expecter) WritePowerLimits(entity interface{}, data interface{}) *CemCEVCInterface_WritePowerLimits_Call { + return &CemCEVCInterface_WritePowerLimits_Call{Call: _e.mock.On("WritePowerLimits", entity, data)} +} + +func (_c *CemCEVCInterface_WritePowerLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, data []api.DurationSlotValue)) *CemCEVCInterface_WritePowerLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].([]api.DurationSlotValue)) + }) + return _c +} + +func (_c *CemCEVCInterface_WritePowerLimits_Call) Return(_a0 error) *CemCEVCInterface_WritePowerLimits_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemCEVCInterface_WritePowerLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, []api.DurationSlotValue) error) *CemCEVCInterface_WritePowerLimits_Call { + _c.Call.Return(run) + return _c +} + +// NewCemCEVCInterface creates a new instance of CemCEVCInterface. 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 NewCemCEVCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemCEVCInterface { + mock := &CemCEVCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemEVCCInterface.go b/usecases/mocks/CemEVCCInterface.go new file mode 100644 index 00000000..2dbce4d5 --- /dev/null +++ b/usecases/mocks/CemEVCCInterface.go @@ -0,0 +1,694 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + eebus_goapi "github.com/enbility/eebus-go/api" + api "github.com/enbility/eebus-go/usecases/api" + + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" +) + +// CemEVCCInterface is an autogenerated mock type for the CemEVCCInterface type +type CemEVCCInterface struct { + mock.Mock +} + +type CemEVCCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemEVCCInterface) EXPECT() *CemEVCCInterface_Expecter { + return &CemEVCCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemEVCCInterface) AddFeatures() { + _m.Called() +} + +// CemEVCCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemEVCCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemEVCCInterface_Expecter) AddFeatures() *CemEVCCInterface_AddFeatures_Call { + return &CemEVCCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemEVCCInterface_AddFeatures_Call) Run(run func()) *CemEVCCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVCCInterface_AddFeatures_Call) Return() *CemEVCCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVCCInterface_AddFeatures_Call) RunAndReturn(run func()) *CemEVCCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemEVCCInterface) AddUseCase() { + _m.Called() +} + +// CemEVCCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemEVCCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemEVCCInterface_Expecter) AddUseCase() *CemEVCCInterface_AddUseCase_Call { + return &CemEVCCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemEVCCInterface_AddUseCase_Call) Run(run func()) *CemEVCCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVCCInterface_AddUseCase_Call) Return() *CemEVCCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVCCInterface_AddUseCase_Call) RunAndReturn(run func()) *CemEVCCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// AsymmetricChargingSupport provides a mock function with given fields: entity +func (_m *CemEVCCInterface) AsymmetricChargingSupport(entity spine_goapi.EntityRemoteInterface) (bool, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for AsymmetricChargingSupport") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_AsymmetricChargingSupport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AsymmetricChargingSupport' +type CemEVCCInterface_AsymmetricChargingSupport_Call struct { + *mock.Call +} + +// AsymmetricChargingSupport is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) AsymmetricChargingSupport(entity interface{}) *CemEVCCInterface_AsymmetricChargingSupport_Call { + return &CemEVCCInterface_AsymmetricChargingSupport_Call{Call: _e.mock.On("AsymmetricChargingSupport", entity)} +} + +func (_c *CemEVCCInterface_AsymmetricChargingSupport_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_AsymmetricChargingSupport_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_AsymmetricChargingSupport_Call) Return(_a0 bool, _a1 error) *CemEVCCInterface_AsymmetricChargingSupport_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_AsymmetricChargingSupport_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemEVCCInterface_AsymmetricChargingSupport_Call { + _c.Call.Return(run) + return _c +} + +// ChargeState provides a mock function with given fields: entity +func (_m *CemEVCCInterface) ChargeState(entity spine_goapi.EntityRemoteInterface) (api.EVChargeStateType, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargeState") + } + + var r0 api.EVChargeStateType + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.EVChargeStateType, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.EVChargeStateType); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(api.EVChargeStateType) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_ChargeState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargeState' +type CemEVCCInterface_ChargeState_Call struct { + *mock.Call +} + +// ChargeState is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) ChargeState(entity interface{}) *CemEVCCInterface_ChargeState_Call { + return &CemEVCCInterface_ChargeState_Call{Call: _e.mock.On("ChargeState", entity)} +} + +func (_c *CemEVCCInterface_ChargeState_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_ChargeState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_ChargeState_Call) Return(_a0 api.EVChargeStateType, _a1 error) *CemEVCCInterface_ChargeState_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_ChargeState_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.EVChargeStateType, error)) *CemEVCCInterface_ChargeState_Call { + _c.Call.Return(run) + return _c +} + +// ChargingPowerLimits provides a mock function with given fields: entity +func (_m *CemEVCCInterface) ChargingPowerLimits(entity spine_goapi.EntityRemoteInterface) (float64, float64, float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ChargingPowerLimits") + } + + var r0 float64 + var r1 float64 + var r2 float64 + var r3 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, float64, float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r1 = rf(entity) + } else { + r1 = ret.Get(1).(float64) + } + + if rf, ok := ret.Get(2).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r2 = rf(entity) + } else { + r2 = ret.Get(2).(float64) + } + + if rf, ok := ret.Get(3).(func(spine_goapi.EntityRemoteInterface) error); ok { + r3 = rf(entity) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// CemEVCCInterface_ChargingPowerLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChargingPowerLimits' +type CemEVCCInterface_ChargingPowerLimits_Call struct { + *mock.Call +} + +// ChargingPowerLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) ChargingPowerLimits(entity interface{}) *CemEVCCInterface_ChargingPowerLimits_Call { + return &CemEVCCInterface_ChargingPowerLimits_Call{Call: _e.mock.On("ChargingPowerLimits", entity)} +} + +func (_c *CemEVCCInterface_ChargingPowerLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_ChargingPowerLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_ChargingPowerLimits_Call) Return(_a0 float64, _a1 float64, _a2 float64, _a3 error) *CemEVCCInterface_ChargingPowerLimits_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *CemEVCCInterface_ChargingPowerLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, float64, float64, error)) *CemEVCCInterface_ChargingPowerLimits_Call { + _c.Call.Return(run) + return _c +} + +// CommunicationStandard provides a mock function with given fields: entity +func (_m *CemEVCCInterface) CommunicationStandard(entity spine_goapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CommunicationStandard") + } + + var r0 model.DeviceConfigurationKeyValueStringType + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) model.DeviceConfigurationKeyValueStringType); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(model.DeviceConfigurationKeyValueStringType) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_CommunicationStandard_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommunicationStandard' +type CemEVCCInterface_CommunicationStandard_Call struct { + *mock.Call +} + +// CommunicationStandard is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) CommunicationStandard(entity interface{}) *CemEVCCInterface_CommunicationStandard_Call { + return &CemEVCCInterface_CommunicationStandard_Call{Call: _e.mock.On("CommunicationStandard", entity)} +} + +func (_c *CemEVCCInterface_CommunicationStandard_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_CommunicationStandard_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_CommunicationStandard_Call) Return(_a0 model.DeviceConfigurationKeyValueStringType, _a1 error) *CemEVCCInterface_CommunicationStandard_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_CommunicationStandard_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (model.DeviceConfigurationKeyValueStringType, error)) *CemEVCCInterface_CommunicationStandard_Call { + _c.Call.Return(run) + return _c +} + +// EVConnected provides a mock function with given fields: entity +func (_m *CemEVCCInterface) EVConnected(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EVConnected") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemEVCCInterface_EVConnected_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EVConnected' +type CemEVCCInterface_EVConnected_Call struct { + *mock.Call +} + +// EVConnected is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) EVConnected(entity interface{}) *CemEVCCInterface_EVConnected_Call { + return &CemEVCCInterface_EVConnected_Call{Call: _e.mock.On("EVConnected", entity)} +} + +func (_c *CemEVCCInterface_EVConnected_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_EVConnected_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_EVConnected_Call) Return(_a0 bool) *CemEVCCInterface_EVConnected_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemEVCCInterface_EVConnected_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemEVCCInterface_EVConnected_Call { + _c.Call.Return(run) + return _c +} + +// Identifications provides a mock function with given fields: entity +func (_m *CemEVCCInterface) Identifications(entity spine_goapi.EntityRemoteInterface) ([]api.IdentificationItem, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Identifications") + } + + var r0 []api.IdentificationItem + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]api.IdentificationItem, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []api.IdentificationItem); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api.IdentificationItem) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_Identifications_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Identifications' +type CemEVCCInterface_Identifications_Call struct { + *mock.Call +} + +// Identifications is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) Identifications(entity interface{}) *CemEVCCInterface_Identifications_Call { + return &CemEVCCInterface_Identifications_Call{Call: _e.mock.On("Identifications", entity)} +} + +func (_c *CemEVCCInterface_Identifications_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_Identifications_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_Identifications_Call) Return(_a0 []api.IdentificationItem, _a1 error) *CemEVCCInterface_Identifications_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_Identifications_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]api.IdentificationItem, error)) *CemEVCCInterface_Identifications_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemEVCCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemEVCCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemEVCCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemEVCCInterface_IsCompatibleEntity_Call { + return &CemEVCCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemEVCCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemEVCCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemEVCCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemEVCCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsInSleepMode provides a mock function with given fields: entity +func (_m *CemEVCCInterface) IsInSleepMode(entity spine_goapi.EntityRemoteInterface) (bool, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsInSleepMode") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_IsInSleepMode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsInSleepMode' +type CemEVCCInterface_IsInSleepMode_Call struct { + *mock.Call +} + +// IsInSleepMode is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) IsInSleepMode(entity interface{}) *CemEVCCInterface_IsInSleepMode_Call { + return &CemEVCCInterface_IsInSleepMode_Call{Call: _e.mock.On("IsInSleepMode", entity)} +} + +func (_c *CemEVCCInterface_IsInSleepMode_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_IsInSleepMode_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_IsInSleepMode_Call) Return(_a0 bool, _a1 error) *CemEVCCInterface_IsInSleepMode_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_IsInSleepMode_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemEVCCInterface_IsInSleepMode_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemEVCCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemEVCCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemEVCCInterface_IsUseCaseSupported_Call { + return &CemEVCCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemEVCCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemEVCCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemEVCCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// ManufacturerData provides a mock function with given fields: entity +func (_m *CemEVCCInterface) ManufacturerData(entity spine_goapi.EntityRemoteInterface) (eebus_goapi.ManufacturerData, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ManufacturerData") + } + + var r0 eebus_goapi.ManufacturerData + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (eebus_goapi.ManufacturerData, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) eebus_goapi.ManufacturerData); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(eebus_goapi.ManufacturerData) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCCInterface_ManufacturerData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ManufacturerData' +type CemEVCCInterface_ManufacturerData_Call struct { + *mock.Call +} + +// ManufacturerData is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCCInterface_Expecter) ManufacturerData(entity interface{}) *CemEVCCInterface_ManufacturerData_Call { + return &CemEVCCInterface_ManufacturerData_Call{Call: _e.mock.On("ManufacturerData", entity)} +} + +func (_c *CemEVCCInterface_ManufacturerData_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCCInterface_ManufacturerData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCCInterface_ManufacturerData_Call) Return(_a0 eebus_goapi.ManufacturerData, _a1 error) *CemEVCCInterface_ManufacturerData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCCInterface_ManufacturerData_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (eebus_goapi.ManufacturerData, error)) *CemEVCCInterface_ManufacturerData_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemEVCCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemEVCCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemEVCCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemEVCCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemEVCCInterface_UpdateUseCaseAvailability_Call { + return &CemEVCCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemEVCCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemEVCCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemEVCCInterface_UpdateUseCaseAvailability_Call) Return() *CemEVCCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVCCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemEVCCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCemEVCCInterface creates a new instance of CemEVCCInterface. 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 NewCemEVCCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemEVCCInterface { + mock := &CemEVCCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemEVCEMInterface.go b/usecases/mocks/CemEVCEMInterface.go new file mode 100644 index 00000000..9ba25f65 --- /dev/null +++ b/usecases/mocks/CemEVCEMInterface.go @@ -0,0 +1,462 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// CemEVCEMInterface is an autogenerated mock type for the CemEVCEMInterface type +type CemEVCEMInterface struct { + mock.Mock +} + +type CemEVCEMInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemEVCEMInterface) EXPECT() *CemEVCEMInterface_Expecter { + return &CemEVCEMInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemEVCEMInterface) AddFeatures() { + _m.Called() +} + +// CemEVCEMInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemEVCEMInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemEVCEMInterface_Expecter) AddFeatures() *CemEVCEMInterface_AddFeatures_Call { + return &CemEVCEMInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemEVCEMInterface_AddFeatures_Call) Run(run func()) *CemEVCEMInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVCEMInterface_AddFeatures_Call) Return() *CemEVCEMInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVCEMInterface_AddFeatures_Call) RunAndReturn(run func()) *CemEVCEMInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemEVCEMInterface) AddUseCase() { + _m.Called() +} + +// CemEVCEMInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemEVCEMInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemEVCEMInterface_Expecter) AddUseCase() *CemEVCEMInterface_AddUseCase_Call { + return &CemEVCEMInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemEVCEMInterface_AddUseCase_Call) Run(run func()) *CemEVCEMInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVCEMInterface_AddUseCase_Call) Return() *CemEVCEMInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVCEMInterface_AddUseCase_Call) RunAndReturn(run func()) *CemEVCEMInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentPerPhase provides a mock function with given fields: entity +func (_m *CemEVCEMInterface) CurrentPerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCEMInterface_CurrentPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentPerPhase' +type CemEVCEMInterface_CurrentPerPhase_Call struct { + *mock.Call +} + +// CurrentPerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCEMInterface_Expecter) CurrentPerPhase(entity interface{}) *CemEVCEMInterface_CurrentPerPhase_Call { + return &CemEVCEMInterface_CurrentPerPhase_Call{Call: _e.mock.On("CurrentPerPhase", entity)} +} + +func (_c *CemEVCEMInterface_CurrentPerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCEMInterface_CurrentPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCEMInterface_CurrentPerPhase_Call) Return(_a0 []float64, _a1 error) *CemEVCEMInterface_CurrentPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCEMInterface_CurrentPerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *CemEVCEMInterface_CurrentPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyCharged provides a mock function with given fields: entity +func (_m *CemEVCEMInterface) EnergyCharged(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyCharged") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCEMInterface_EnergyCharged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyCharged' +type CemEVCEMInterface_EnergyCharged_Call struct { + *mock.Call +} + +// EnergyCharged is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCEMInterface_Expecter) EnergyCharged(entity interface{}) *CemEVCEMInterface_EnergyCharged_Call { + return &CemEVCEMInterface_EnergyCharged_Call{Call: _e.mock.On("EnergyCharged", entity)} +} + +func (_c *CemEVCEMInterface_EnergyCharged_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCEMInterface_EnergyCharged_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCEMInterface_EnergyCharged_Call) Return(_a0 float64, _a1 error) *CemEVCEMInterface_EnergyCharged_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCEMInterface_EnergyCharged_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemEVCEMInterface_EnergyCharged_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemEVCEMInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemEVCEMInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemEVCEMInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCEMInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemEVCEMInterface_IsCompatibleEntity_Call { + return &CemEVCEMInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemEVCEMInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCEMInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCEMInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemEVCEMInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemEVCEMInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemEVCEMInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemEVCEMInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCEMInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemEVCEMInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemEVCEMInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemEVCEMInterface_IsUseCaseSupported_Call { + return &CemEVCEMInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemEVCEMInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemEVCEMInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCEMInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemEVCEMInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCEMInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemEVCEMInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PhasesConnected provides a mock function with given fields: entity +func (_m *CemEVCEMInterface) PhasesConnected(entity spine_goapi.EntityRemoteInterface) (uint, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PhasesConnected") + } + + var r0 uint + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (uint, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) uint); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(uint) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCEMInterface_PhasesConnected_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PhasesConnected' +type CemEVCEMInterface_PhasesConnected_Call struct { + *mock.Call +} + +// PhasesConnected is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCEMInterface_Expecter) PhasesConnected(entity interface{}) *CemEVCEMInterface_PhasesConnected_Call { + return &CemEVCEMInterface_PhasesConnected_Call{Call: _e.mock.On("PhasesConnected", entity)} +} + +func (_c *CemEVCEMInterface_PhasesConnected_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCEMInterface_PhasesConnected_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCEMInterface_PhasesConnected_Call) Return(_a0 uint, _a1 error) *CemEVCEMInterface_PhasesConnected_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCEMInterface_PhasesConnected_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (uint, error)) *CemEVCEMInterface_PhasesConnected_Call { + _c.Call.Return(run) + return _c +} + +// PowerPerPhase provides a mock function with given fields: entity +func (_m *CemEVCEMInterface) PowerPerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVCEMInterface_PowerPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerPerPhase' +type CemEVCEMInterface_PowerPerPhase_Call struct { + *mock.Call +} + +// PowerPerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVCEMInterface_Expecter) PowerPerPhase(entity interface{}) *CemEVCEMInterface_PowerPerPhase_Call { + return &CemEVCEMInterface_PowerPerPhase_Call{Call: _e.mock.On("PowerPerPhase", entity)} +} + +func (_c *CemEVCEMInterface_PowerPerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVCEMInterface_PowerPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVCEMInterface_PowerPerPhase_Call) Return(_a0 []float64, _a1 error) *CemEVCEMInterface_PowerPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVCEMInterface_PowerPerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *CemEVCEMInterface_PowerPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemEVCEMInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemEVCEMInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemEVCEMInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemEVCEMInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemEVCEMInterface_UpdateUseCaseAvailability_Call { + return &CemEVCEMInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemEVCEMInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemEVCEMInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemEVCEMInterface_UpdateUseCaseAvailability_Call) Return() *CemEVCEMInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVCEMInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemEVCEMInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCemEVCEMInterface creates a new instance of CemEVCEMInterface. 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 NewCemEVCEMInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemEVCEMInterface { + mock := &CemEVCEMInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemEVSECCInterface.go b/usecases/mocks/CemEVSECCInterface.go new file mode 100644 index 00000000..2ae55823 --- /dev/null +++ b/usecases/mocks/CemEVSECCInterface.go @@ -0,0 +1,357 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + eebus_goapi "github.com/enbility/eebus-go/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" +) + +// CemEVSECCInterface is an autogenerated mock type for the CemEVSECCInterface type +type CemEVSECCInterface struct { + mock.Mock +} + +type CemEVSECCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemEVSECCInterface) EXPECT() *CemEVSECCInterface_Expecter { + return &CemEVSECCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemEVSECCInterface) AddFeatures() { + _m.Called() +} + +// CemEVSECCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemEVSECCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemEVSECCInterface_Expecter) AddFeatures() *CemEVSECCInterface_AddFeatures_Call { + return &CemEVSECCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemEVSECCInterface_AddFeatures_Call) Run(run func()) *CemEVSECCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVSECCInterface_AddFeatures_Call) Return() *CemEVSECCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVSECCInterface_AddFeatures_Call) RunAndReturn(run func()) *CemEVSECCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemEVSECCInterface) AddUseCase() { + _m.Called() +} + +// CemEVSECCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemEVSECCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemEVSECCInterface_Expecter) AddUseCase() *CemEVSECCInterface_AddUseCase_Call { + return &CemEVSECCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemEVSECCInterface_AddUseCase_Call) Run(run func()) *CemEVSECCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVSECCInterface_AddUseCase_Call) Return() *CemEVSECCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVSECCInterface_AddUseCase_Call) RunAndReturn(run func()) *CemEVSECCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemEVSECCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemEVSECCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemEVSECCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVSECCInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemEVSECCInterface_IsCompatibleEntity_Call { + return &CemEVSECCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemEVSECCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVSECCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSECCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemEVSECCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemEVSECCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemEVSECCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemEVSECCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVSECCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemEVSECCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemEVSECCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemEVSECCInterface_IsUseCaseSupported_Call { + return &CemEVSECCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemEVSECCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemEVSECCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSECCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemEVSECCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVSECCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemEVSECCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// ManufacturerData provides a mock function with given fields: entity +func (_m *CemEVSECCInterface) ManufacturerData(entity spine_goapi.EntityRemoteInterface) (eebus_goapi.ManufacturerData, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ManufacturerData") + } + + var r0 eebus_goapi.ManufacturerData + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (eebus_goapi.ManufacturerData, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) eebus_goapi.ManufacturerData); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(eebus_goapi.ManufacturerData) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVSECCInterface_ManufacturerData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ManufacturerData' +type CemEVSECCInterface_ManufacturerData_Call struct { + *mock.Call +} + +// ManufacturerData is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVSECCInterface_Expecter) ManufacturerData(entity interface{}) *CemEVSECCInterface_ManufacturerData_Call { + return &CemEVSECCInterface_ManufacturerData_Call{Call: _e.mock.On("ManufacturerData", entity)} +} + +func (_c *CemEVSECCInterface_ManufacturerData_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVSECCInterface_ManufacturerData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSECCInterface_ManufacturerData_Call) Return(_a0 eebus_goapi.ManufacturerData, _a1 error) *CemEVSECCInterface_ManufacturerData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVSECCInterface_ManufacturerData_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (eebus_goapi.ManufacturerData, error)) *CemEVSECCInterface_ManufacturerData_Call { + _c.Call.Return(run) + return _c +} + +// OperatingState provides a mock function with given fields: entity +func (_m *CemEVSECCInterface) OperatingState(entity spine_goapi.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for OperatingState") + } + + var r0 model.DeviceDiagnosisOperatingStateType + var r1 string + var r2 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) model.DeviceDiagnosisOperatingStateType); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(model.DeviceDiagnosisOperatingStateType) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) string); ok { + r1 = rf(entity) + } else { + r1 = ret.Get(1).(string) + } + + if rf, ok := ret.Get(2).(func(spine_goapi.EntityRemoteInterface) error); ok { + r2 = rf(entity) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// CemEVSECCInterface_OperatingState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OperatingState' +type CemEVSECCInterface_OperatingState_Call struct { + *mock.Call +} + +// OperatingState is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVSECCInterface_Expecter) OperatingState(entity interface{}) *CemEVSECCInterface_OperatingState_Call { + return &CemEVSECCInterface_OperatingState_Call{Call: _e.mock.On("OperatingState", entity)} +} + +func (_c *CemEVSECCInterface_OperatingState_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVSECCInterface_OperatingState_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSECCInterface_OperatingState_Call) Return(_a0 model.DeviceDiagnosisOperatingStateType, _a1 string, _a2 error) *CemEVSECCInterface_OperatingState_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *CemEVSECCInterface_OperatingState_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (model.DeviceDiagnosisOperatingStateType, string, error)) *CemEVSECCInterface_OperatingState_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemEVSECCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemEVSECCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemEVSECCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemEVSECCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemEVSECCInterface_UpdateUseCaseAvailability_Call { + return &CemEVSECCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemEVSECCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemEVSECCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemEVSECCInterface_UpdateUseCaseAvailability_Call) Return() *CemEVSECCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVSECCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemEVSECCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCemEVSECCInterface creates a new instance of CemEVSECCInterface. 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 NewCemEVSECCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemEVSECCInterface { + mock := &CemEVSECCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemEVSOCInterface.go b/usecases/mocks/CemEVSOCInterface.go new file mode 100644 index 00000000..e42987bb --- /dev/null +++ b/usecases/mocks/CemEVSOCInterface.go @@ -0,0 +1,290 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// CemEVSOCInterface is an autogenerated mock type for the CemEVSOCInterface type +type CemEVSOCInterface struct { + mock.Mock +} + +type CemEVSOCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemEVSOCInterface) EXPECT() *CemEVSOCInterface_Expecter { + return &CemEVSOCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemEVSOCInterface) AddFeatures() { + _m.Called() +} + +// CemEVSOCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemEVSOCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemEVSOCInterface_Expecter) AddFeatures() *CemEVSOCInterface_AddFeatures_Call { + return &CemEVSOCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemEVSOCInterface_AddFeatures_Call) Run(run func()) *CemEVSOCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVSOCInterface_AddFeatures_Call) Return() *CemEVSOCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVSOCInterface_AddFeatures_Call) RunAndReturn(run func()) *CemEVSOCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemEVSOCInterface) AddUseCase() { + _m.Called() +} + +// CemEVSOCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemEVSOCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemEVSOCInterface_Expecter) AddUseCase() *CemEVSOCInterface_AddUseCase_Call { + return &CemEVSOCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemEVSOCInterface_AddUseCase_Call) Run(run func()) *CemEVSOCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemEVSOCInterface_AddUseCase_Call) Return() *CemEVSOCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVSOCInterface_AddUseCase_Call) RunAndReturn(run func()) *CemEVSOCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemEVSOCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemEVSOCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemEVSOCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVSOCInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemEVSOCInterface_IsCompatibleEntity_Call { + return &CemEVSOCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemEVSOCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVSOCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSOCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemEVSOCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemEVSOCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemEVSOCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemEVSOCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVSOCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemEVSOCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemEVSOCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemEVSOCInterface_IsUseCaseSupported_Call { + return &CemEVSOCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemEVSOCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemEVSOCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSOCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemEVSOCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVSOCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemEVSOCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// StateOfCharge provides a mock function with given fields: entity +func (_m *CemEVSOCInterface) StateOfCharge(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for StateOfCharge") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemEVSOCInterface_StateOfCharge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StateOfCharge' +type CemEVSOCInterface_StateOfCharge_Call struct { + *mock.Call +} + +// StateOfCharge is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemEVSOCInterface_Expecter) StateOfCharge(entity interface{}) *CemEVSOCInterface_StateOfCharge_Call { + return &CemEVSOCInterface_StateOfCharge_Call{Call: _e.mock.On("StateOfCharge", entity)} +} + +func (_c *CemEVSOCInterface_StateOfCharge_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemEVSOCInterface_StateOfCharge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemEVSOCInterface_StateOfCharge_Call) Return(_a0 float64, _a1 error) *CemEVSOCInterface_StateOfCharge_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemEVSOCInterface_StateOfCharge_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemEVSOCInterface_StateOfCharge_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemEVSOCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemEVSOCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemEVSOCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemEVSOCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemEVSOCInterface_UpdateUseCaseAvailability_Call { + return &CemEVSOCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemEVSOCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemEVSOCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemEVSOCInterface_UpdateUseCaseAvailability_Call) Return() *CemEVSOCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemEVSOCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemEVSOCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCemEVSOCInterface creates a new instance of CemEVSOCInterface. 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 NewCemEVSOCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemEVSOCInterface { + mock := &CemEVSOCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemOPEVInterface.go b/usecases/mocks/CemOPEVInterface.go new file mode 100644 index 00000000..cf8efd4d --- /dev/null +++ b/usecases/mocks/CemOPEVInterface.go @@ -0,0 +1,431 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" +) + +// CemOPEVInterface is an autogenerated mock type for the CemOPEVInterface type +type CemOPEVInterface struct { + mock.Mock +} + +type CemOPEVInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemOPEVInterface) EXPECT() *CemOPEVInterface_Expecter { + return &CemOPEVInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemOPEVInterface) AddFeatures() { + _m.Called() +} + +// CemOPEVInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemOPEVInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemOPEVInterface_Expecter) AddFeatures() *CemOPEVInterface_AddFeatures_Call { + return &CemOPEVInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemOPEVInterface_AddFeatures_Call) Run(run func()) *CemOPEVInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemOPEVInterface_AddFeatures_Call) Return() *CemOPEVInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemOPEVInterface_AddFeatures_Call) RunAndReturn(run func()) *CemOPEVInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemOPEVInterface) AddUseCase() { + _m.Called() +} + +// CemOPEVInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemOPEVInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemOPEVInterface_Expecter) AddUseCase() *CemOPEVInterface_AddUseCase_Call { + return &CemOPEVInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemOPEVInterface_AddUseCase_Call) Run(run func()) *CemOPEVInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemOPEVInterface_AddUseCase_Call) Return() *CemOPEVInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemOPEVInterface_AddUseCase_Call) RunAndReturn(run func()) *CemOPEVInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentLimits provides a mock function with given fields: entity +func (_m *CemOPEVInterface) CurrentLimits(entity spine_goapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentLimits") + } + + var r0 []float64 + var r1 []float64 + var r2 []float64 + var r3 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, []float64, []float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r1 = rf(entity) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]float64) + } + } + + if rf, ok := ret.Get(2).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r2 = rf(entity) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]float64) + } + } + + if rf, ok := ret.Get(3).(func(spine_goapi.EntityRemoteInterface) error); ok { + r3 = rf(entity) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// CemOPEVInterface_CurrentLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentLimits' +type CemOPEVInterface_CurrentLimits_Call struct { + *mock.Call +} + +// CurrentLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemOPEVInterface_Expecter) CurrentLimits(entity interface{}) *CemOPEVInterface_CurrentLimits_Call { + return &CemOPEVInterface_CurrentLimits_Call{Call: _e.mock.On("CurrentLimits", entity)} +} + +func (_c *CemOPEVInterface_CurrentLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemOPEVInterface_CurrentLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOPEVInterface_CurrentLimits_Call) Return(_a0 []float64, _a1 []float64, _a2 []float64, _a3 error) *CemOPEVInterface_CurrentLimits_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *CemOPEVInterface_CurrentLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, []float64, []float64, error)) *CemOPEVInterface_CurrentLimits_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemOPEVInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemOPEVInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemOPEVInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemOPEVInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemOPEVInterface_IsCompatibleEntity_Call { + return &CemOPEVInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemOPEVInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemOPEVInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOPEVInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemOPEVInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemOPEVInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemOPEVInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemOPEVInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemOPEVInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemOPEVInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemOPEVInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemOPEVInterface_IsUseCaseSupported_Call { + return &CemOPEVInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemOPEVInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemOPEVInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOPEVInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemOPEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemOPEVInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemOPEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// LoadControlLimits provides a mock function with given fields: entity +func (_m *CemOPEVInterface) LoadControlLimits(entity spine_goapi.EntityRemoteInterface) ([]api.LoadLimitsPhase, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for LoadControlLimits") + } + + var r0 []api.LoadLimitsPhase + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]api.LoadLimitsPhase, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []api.LoadLimitsPhase); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api.LoadLimitsPhase) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemOPEVInterface_LoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadControlLimits' +type CemOPEVInterface_LoadControlLimits_Call struct { + *mock.Call +} + +// LoadControlLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemOPEVInterface_Expecter) LoadControlLimits(entity interface{}) *CemOPEVInterface_LoadControlLimits_Call { + return &CemOPEVInterface_LoadControlLimits_Call{Call: _e.mock.On("LoadControlLimits", entity)} +} + +func (_c *CemOPEVInterface_LoadControlLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemOPEVInterface_LoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOPEVInterface_LoadControlLimits_Call) Return(limits []api.LoadLimitsPhase, resultErr error) *CemOPEVInterface_LoadControlLimits_Call { + _c.Call.Return(limits, resultErr) + return _c +} + +func (_c *CemOPEVInterface_LoadControlLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]api.LoadLimitsPhase, error)) *CemOPEVInterface_LoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemOPEVInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemOPEVInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemOPEVInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemOPEVInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemOPEVInterface_UpdateUseCaseAvailability_Call { + return &CemOPEVInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemOPEVInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemOPEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemOPEVInterface_UpdateUseCaseAvailability_Call) Return() *CemOPEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemOPEVInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemOPEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// WriteLoadControlLimits provides a mock function with given fields: entity, limits +func (_m *CemOPEVInterface) WriteLoadControlLimits(entity spine_goapi.EntityRemoteInterface, limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limits) + + if len(ret) == 0 { + panic("no return value specified for WriteLoadControlLimits") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) (*model.MsgCounterType, error)); ok { + return rf(entity, limits) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) *model.MsgCounterType); ok { + r0 = rf(entity, limits) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) error); ok { + r1 = rf(entity, limits) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemOPEVInterface_WriteLoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteLoadControlLimits' +type CemOPEVInterface_WriteLoadControlLimits_Call struct { + *mock.Call +} + +// WriteLoadControlLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - limits []api.LoadLimitsPhase +func (_e *CemOPEVInterface_Expecter) WriteLoadControlLimits(entity interface{}, limits interface{}) *CemOPEVInterface_WriteLoadControlLimits_Call { + return &CemOPEVInterface_WriteLoadControlLimits_Call{Call: _e.mock.On("WriteLoadControlLimits", entity, limits)} +} + +func (_c *CemOPEVInterface_WriteLoadControlLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, limits []api.LoadLimitsPhase)) *CemOPEVInterface_WriteLoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].([]api.LoadLimitsPhase)) + }) + return _c +} + +func (_c *CemOPEVInterface_WriteLoadControlLimits_Call) Return(_a0 *model.MsgCounterType, _a1 error) *CemOPEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemOPEVInterface_WriteLoadControlLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) (*model.MsgCounterType, error)) *CemOPEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// NewCemOPEVInterface creates a new instance of CemOPEVInterface. 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 NewCemOPEVInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemOPEVInterface { + mock := &CemOPEVInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemOSCEVInterface.go b/usecases/mocks/CemOSCEVInterface.go new file mode 100644 index 00000000..a404dc3a --- /dev/null +++ b/usecases/mocks/CemOSCEVInterface.go @@ -0,0 +1,431 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" +) + +// CemOSCEVInterface is an autogenerated mock type for the CemOSCEVInterface type +type CemOSCEVInterface struct { + mock.Mock +} + +type CemOSCEVInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemOSCEVInterface) EXPECT() *CemOSCEVInterface_Expecter { + return &CemOSCEVInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemOSCEVInterface) AddFeatures() { + _m.Called() +} + +// CemOSCEVInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemOSCEVInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemOSCEVInterface_Expecter) AddFeatures() *CemOSCEVInterface_AddFeatures_Call { + return &CemOSCEVInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemOSCEVInterface_AddFeatures_Call) Run(run func()) *CemOSCEVInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemOSCEVInterface_AddFeatures_Call) Return() *CemOSCEVInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemOSCEVInterface_AddFeatures_Call) RunAndReturn(run func()) *CemOSCEVInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemOSCEVInterface) AddUseCase() { + _m.Called() +} + +// CemOSCEVInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemOSCEVInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemOSCEVInterface_Expecter) AddUseCase() *CemOSCEVInterface_AddUseCase_Call { + return &CemOSCEVInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemOSCEVInterface_AddUseCase_Call) Run(run func()) *CemOSCEVInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemOSCEVInterface_AddUseCase_Call) Return() *CemOSCEVInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemOSCEVInterface_AddUseCase_Call) RunAndReturn(run func()) *CemOSCEVInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentLimits provides a mock function with given fields: entity +func (_m *CemOSCEVInterface) CurrentLimits(entity spine_goapi.EntityRemoteInterface) ([]float64, []float64, []float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentLimits") + } + + var r0 []float64 + var r1 []float64 + var r2 []float64 + var r3 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, []float64, []float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r1 = rf(entity) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]float64) + } + } + + if rf, ok := ret.Get(2).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r2 = rf(entity) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).([]float64) + } + } + + if rf, ok := ret.Get(3).(func(spine_goapi.EntityRemoteInterface) error); ok { + r3 = rf(entity) + } else { + r3 = ret.Error(3) + } + + return r0, r1, r2, r3 +} + +// CemOSCEVInterface_CurrentLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentLimits' +type CemOSCEVInterface_CurrentLimits_Call struct { + *mock.Call +} + +// CurrentLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemOSCEVInterface_Expecter) CurrentLimits(entity interface{}) *CemOSCEVInterface_CurrentLimits_Call { + return &CemOSCEVInterface_CurrentLimits_Call{Call: _e.mock.On("CurrentLimits", entity)} +} + +func (_c *CemOSCEVInterface_CurrentLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemOSCEVInterface_CurrentLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOSCEVInterface_CurrentLimits_Call) Return(_a0 []float64, _a1 []float64, _a2 []float64, _a3 error) *CemOSCEVInterface_CurrentLimits_Call { + _c.Call.Return(_a0, _a1, _a2, _a3) + return _c +} + +func (_c *CemOSCEVInterface_CurrentLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, []float64, []float64, error)) *CemOSCEVInterface_CurrentLimits_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemOSCEVInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemOSCEVInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemOSCEVInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemOSCEVInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemOSCEVInterface_IsCompatibleEntity_Call { + return &CemOSCEVInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemOSCEVInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemOSCEVInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOSCEVInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemOSCEVInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemOSCEVInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemOSCEVInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemOSCEVInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemOSCEVInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemOSCEVInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemOSCEVInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemOSCEVInterface_IsUseCaseSupported_Call { + return &CemOSCEVInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemOSCEVInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemOSCEVInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOSCEVInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemOSCEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemOSCEVInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemOSCEVInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// LoadControlLimits provides a mock function with given fields: entity +func (_m *CemOSCEVInterface) LoadControlLimits(entity spine_goapi.EntityRemoteInterface) ([]api.LoadLimitsPhase, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for LoadControlLimits") + } + + var r0 []api.LoadLimitsPhase + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]api.LoadLimitsPhase, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []api.LoadLimitsPhase); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api.LoadLimitsPhase) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemOSCEVInterface_LoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadControlLimits' +type CemOSCEVInterface_LoadControlLimits_Call struct { + *mock.Call +} + +// LoadControlLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemOSCEVInterface_Expecter) LoadControlLimits(entity interface{}) *CemOSCEVInterface_LoadControlLimits_Call { + return &CemOSCEVInterface_LoadControlLimits_Call{Call: _e.mock.On("LoadControlLimits", entity)} +} + +func (_c *CemOSCEVInterface_LoadControlLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemOSCEVInterface_LoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemOSCEVInterface_LoadControlLimits_Call) Return(limits []api.LoadLimitsPhase, resultErr error) *CemOSCEVInterface_LoadControlLimits_Call { + _c.Call.Return(limits, resultErr) + return _c +} + +func (_c *CemOSCEVInterface_LoadControlLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]api.LoadLimitsPhase, error)) *CemOSCEVInterface_LoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemOSCEVInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemOSCEVInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemOSCEVInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemOSCEVInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemOSCEVInterface_UpdateUseCaseAvailability_Call { + return &CemOSCEVInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemOSCEVInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemOSCEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemOSCEVInterface_UpdateUseCaseAvailability_Call) Return() *CemOSCEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemOSCEVInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemOSCEVInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// WriteLoadControlLimits provides a mock function with given fields: entity, limits +func (_m *CemOSCEVInterface) WriteLoadControlLimits(entity spine_goapi.EntityRemoteInterface, limits []api.LoadLimitsPhase) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limits) + + if len(ret) == 0 { + panic("no return value specified for WriteLoadControlLimits") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) (*model.MsgCounterType, error)); ok { + return rf(entity, limits) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) *model.MsgCounterType); ok { + r0 = rf(entity, limits) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) error); ok { + r1 = rf(entity, limits) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemOSCEVInterface_WriteLoadControlLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteLoadControlLimits' +type CemOSCEVInterface_WriteLoadControlLimits_Call struct { + *mock.Call +} + +// WriteLoadControlLimits is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - limits []api.LoadLimitsPhase +func (_e *CemOSCEVInterface_Expecter) WriteLoadControlLimits(entity interface{}, limits interface{}) *CemOSCEVInterface_WriteLoadControlLimits_Call { + return &CemOSCEVInterface_WriteLoadControlLimits_Call{Call: _e.mock.On("WriteLoadControlLimits", entity, limits)} +} + +func (_c *CemOSCEVInterface_WriteLoadControlLimits_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, limits []api.LoadLimitsPhase)) *CemOSCEVInterface_WriteLoadControlLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].([]api.LoadLimitsPhase)) + }) + return _c +} + +func (_c *CemOSCEVInterface_WriteLoadControlLimits_Call) Return(_a0 *model.MsgCounterType, _a1 error) *CemOSCEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemOSCEVInterface_WriteLoadControlLimits_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, []api.LoadLimitsPhase) (*model.MsgCounterType, error)) *CemOSCEVInterface_WriteLoadControlLimits_Call { + _c.Call.Return(run) + return _c +} + +// NewCemOSCEVInterface creates a new instance of CemOSCEVInterface. 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 NewCemOSCEVInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemOSCEVInterface { + mock := &CemOSCEVInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemVABDInterface.go b/usecases/mocks/CemVABDInterface.go new file mode 100644 index 00000000..29d7f944 --- /dev/null +++ b/usecases/mocks/CemVABDInterface.go @@ -0,0 +1,458 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// CemVABDInterface is an autogenerated mock type for the CemVABDInterface type +type CemVABDInterface struct { + mock.Mock +} + +type CemVABDInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemVABDInterface) EXPECT() *CemVABDInterface_Expecter { + return &CemVABDInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemVABDInterface) AddFeatures() { + _m.Called() +} + +// CemVABDInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemVABDInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemVABDInterface_Expecter) AddFeatures() *CemVABDInterface_AddFeatures_Call { + return &CemVABDInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemVABDInterface_AddFeatures_Call) Run(run func()) *CemVABDInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemVABDInterface_AddFeatures_Call) Return() *CemVABDInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemVABDInterface_AddFeatures_Call) RunAndReturn(run func()) *CemVABDInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemVABDInterface) AddUseCase() { + _m.Called() +} + +// CemVABDInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemVABDInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemVABDInterface_Expecter) AddUseCase() *CemVABDInterface_AddUseCase_Call { + return &CemVABDInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemVABDInterface_AddUseCase_Call) Run(run func()) *CemVABDInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemVABDInterface_AddUseCase_Call) Return() *CemVABDInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemVABDInterface_AddUseCase_Call) RunAndReturn(run func()) *CemVABDInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyCharged provides a mock function with given fields: entity +func (_m *CemVABDInterface) EnergyCharged(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyCharged") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVABDInterface_EnergyCharged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyCharged' +type CemVABDInterface_EnergyCharged_Call struct { + *mock.Call +} + +// EnergyCharged is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVABDInterface_Expecter) EnergyCharged(entity interface{}) *CemVABDInterface_EnergyCharged_Call { + return &CemVABDInterface_EnergyCharged_Call{Call: _e.mock.On("EnergyCharged", entity)} +} + +func (_c *CemVABDInterface_EnergyCharged_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVABDInterface_EnergyCharged_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVABDInterface_EnergyCharged_Call) Return(_a0 float64, _a1 error) *CemVABDInterface_EnergyCharged_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVABDInterface_EnergyCharged_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVABDInterface_EnergyCharged_Call { + _c.Call.Return(run) + return _c +} + +// EnergyDischarged provides a mock function with given fields: entity +func (_m *CemVABDInterface) EnergyDischarged(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyDischarged") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVABDInterface_EnergyDischarged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyDischarged' +type CemVABDInterface_EnergyDischarged_Call struct { + *mock.Call +} + +// EnergyDischarged is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVABDInterface_Expecter) EnergyDischarged(entity interface{}) *CemVABDInterface_EnergyDischarged_Call { + return &CemVABDInterface_EnergyDischarged_Call{Call: _e.mock.On("EnergyDischarged", entity)} +} + +func (_c *CemVABDInterface_EnergyDischarged_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVABDInterface_EnergyDischarged_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVABDInterface_EnergyDischarged_Call) Return(_a0 float64, _a1 error) *CemVABDInterface_EnergyDischarged_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVABDInterface_EnergyDischarged_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVABDInterface_EnergyDischarged_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemVABDInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemVABDInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemVABDInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVABDInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemVABDInterface_IsCompatibleEntity_Call { + return &CemVABDInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemVABDInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVABDInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVABDInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemVABDInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemVABDInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemVABDInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemVABDInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVABDInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemVABDInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemVABDInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemVABDInterface_IsUseCaseSupported_Call { + return &CemVABDInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemVABDInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemVABDInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVABDInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemVABDInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVABDInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemVABDInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *CemVABDInterface) Power(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVABDInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type CemVABDInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVABDInterface_Expecter) Power(entity interface{}) *CemVABDInterface_Power_Call { + return &CemVABDInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *CemVABDInterface_Power_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVABDInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVABDInterface_Power_Call) Return(_a0 float64, _a1 error) *CemVABDInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVABDInterface_Power_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVABDInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// StateOfCharge provides a mock function with given fields: entity +func (_m *CemVABDInterface) StateOfCharge(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for StateOfCharge") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVABDInterface_StateOfCharge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StateOfCharge' +type CemVABDInterface_StateOfCharge_Call struct { + *mock.Call +} + +// StateOfCharge is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVABDInterface_Expecter) StateOfCharge(entity interface{}) *CemVABDInterface_StateOfCharge_Call { + return &CemVABDInterface_StateOfCharge_Call{Call: _e.mock.On("StateOfCharge", entity)} +} + +func (_c *CemVABDInterface_StateOfCharge_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVABDInterface_StateOfCharge_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVABDInterface_StateOfCharge_Call) Return(_a0 float64, _a1 error) *CemVABDInterface_StateOfCharge_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVABDInterface_StateOfCharge_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVABDInterface_StateOfCharge_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemVABDInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemVABDInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemVABDInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemVABDInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemVABDInterface_UpdateUseCaseAvailability_Call { + return &CemVABDInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemVABDInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemVABDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemVABDInterface_UpdateUseCaseAvailability_Call) Return() *CemVABDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemVABDInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemVABDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCemVABDInterface creates a new instance of CemVABDInterface. 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 NewCemVABDInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemVABDInterface { + mock := &CemVABDInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CemVAPDInterface.go b/usecases/mocks/CemVAPDInterface.go new file mode 100644 index 00000000..78149761 --- /dev/null +++ b/usecases/mocks/CemVAPDInterface.go @@ -0,0 +1,402 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// CemVAPDInterface is an autogenerated mock type for the CemVAPDInterface type +type CemVAPDInterface struct { + mock.Mock +} + +type CemVAPDInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CemVAPDInterface) EXPECT() *CemVAPDInterface_Expecter { + return &CemVAPDInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CemVAPDInterface) AddFeatures() { + _m.Called() +} + +// CemVAPDInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CemVAPDInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CemVAPDInterface_Expecter) AddFeatures() *CemVAPDInterface_AddFeatures_Call { + return &CemVAPDInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CemVAPDInterface_AddFeatures_Call) Run(run func()) *CemVAPDInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemVAPDInterface_AddFeatures_Call) Return() *CemVAPDInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CemVAPDInterface_AddFeatures_Call) RunAndReturn(run func()) *CemVAPDInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CemVAPDInterface) AddUseCase() { + _m.Called() +} + +// CemVAPDInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CemVAPDInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CemVAPDInterface_Expecter) AddUseCase() *CemVAPDInterface_AddUseCase_Call { + return &CemVAPDInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CemVAPDInterface_AddUseCase_Call) Run(run func()) *CemVAPDInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CemVAPDInterface_AddUseCase_Call) Return() *CemVAPDInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CemVAPDInterface_AddUseCase_Call) RunAndReturn(run func()) *CemVAPDInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CemVAPDInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CemVAPDInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CemVAPDInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVAPDInterface_Expecter) IsCompatibleEntity(entity interface{}) *CemVAPDInterface_IsCompatibleEntity_Call { + return &CemVAPDInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CemVAPDInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVAPDInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVAPDInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CemVAPDInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CemVAPDInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CemVAPDInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CemVAPDInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVAPDInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CemVAPDInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CemVAPDInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CemVAPDInterface_IsUseCaseSupported_Call { + return &CemVAPDInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CemVAPDInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CemVAPDInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVAPDInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CemVAPDInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVAPDInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CemVAPDInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PVYieldTotal provides a mock function with given fields: entity +func (_m *CemVAPDInterface) PVYieldTotal(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PVYieldTotal") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVAPDInterface_PVYieldTotal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PVYieldTotal' +type CemVAPDInterface_PVYieldTotal_Call struct { + *mock.Call +} + +// PVYieldTotal is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVAPDInterface_Expecter) PVYieldTotal(entity interface{}) *CemVAPDInterface_PVYieldTotal_Call { + return &CemVAPDInterface_PVYieldTotal_Call{Call: _e.mock.On("PVYieldTotal", entity)} +} + +func (_c *CemVAPDInterface_PVYieldTotal_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVAPDInterface_PVYieldTotal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVAPDInterface_PVYieldTotal_Call) Return(_a0 float64, _a1 error) *CemVAPDInterface_PVYieldTotal_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVAPDInterface_PVYieldTotal_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVAPDInterface_PVYieldTotal_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *CemVAPDInterface) Power(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVAPDInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type CemVAPDInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVAPDInterface_Expecter) Power(entity interface{}) *CemVAPDInterface_Power_Call { + return &CemVAPDInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *CemVAPDInterface_Power_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVAPDInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVAPDInterface_Power_Call) Return(_a0 float64, _a1 error) *CemVAPDInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVAPDInterface_Power_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVAPDInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// PowerNominalPeak provides a mock function with given fields: entity +func (_m *CemVAPDInterface) PowerNominalPeak(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerNominalPeak") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CemVAPDInterface_PowerNominalPeak_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerNominalPeak' +type CemVAPDInterface_PowerNominalPeak_Call struct { + *mock.Call +} + +// PowerNominalPeak is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CemVAPDInterface_Expecter) PowerNominalPeak(entity interface{}) *CemVAPDInterface_PowerNominalPeak_Call { + return &CemVAPDInterface_PowerNominalPeak_Call{Call: _e.mock.On("PowerNominalPeak", entity)} +} + +func (_c *CemVAPDInterface_PowerNominalPeak_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CemVAPDInterface_PowerNominalPeak_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CemVAPDInterface_PowerNominalPeak_Call) Return(_a0 float64, _a1 error) *CemVAPDInterface_PowerNominalPeak_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CemVAPDInterface_PowerNominalPeak_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *CemVAPDInterface_PowerNominalPeak_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CemVAPDInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CemVAPDInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CemVAPDInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CemVAPDInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CemVAPDInterface_UpdateUseCaseAvailability_Call { + return &CemVAPDInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CemVAPDInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CemVAPDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CemVAPDInterface_UpdateUseCaseAvailability_Call) Return() *CemVAPDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CemVAPDInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CemVAPDInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCemVAPDInterface creates a new instance of CemVAPDInterface. 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 NewCemVAPDInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CemVAPDInterface { + mock := &CemVAPDInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CsLPCInterface.go b/usecases/mocks/CsLPCInterface.go new file mode 100644 index 00000000..c1fc5807 --- /dev/null +++ b/usecases/mocks/CsLPCInterface.go @@ -0,0 +1,787 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" + + time "time" +) + +// CsLPCInterface is an autogenerated mock type for the CsLPCInterface type +type CsLPCInterface struct { + mock.Mock +} + +type CsLPCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CsLPCInterface) EXPECT() *CsLPCInterface_Expecter { + return &CsLPCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CsLPCInterface) AddFeatures() { + _m.Called() +} + +// CsLPCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CsLPCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) AddFeatures() *CsLPCInterface_AddFeatures_Call { + return &CsLPCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CsLPCInterface_AddFeatures_Call) Run(run func()) *CsLPCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_AddFeatures_Call) Return() *CsLPCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPCInterface_AddFeatures_Call) RunAndReturn(run func()) *CsLPCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CsLPCInterface) AddUseCase() { + _m.Called() +} + +// CsLPCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CsLPCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) AddUseCase() *CsLPCInterface_AddUseCase_Call { + return &CsLPCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CsLPCInterface_AddUseCase_Call) Run(run func()) *CsLPCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_AddUseCase_Call) Return() *CsLPCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPCInterface_AddUseCase_Call) RunAndReturn(run func()) *CsLPCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ApproveOrDenyConsumptionLimit provides a mock function with given fields: msgCounter, approve, reason +func (_m *CsLPCInterface) ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + _m.Called(msgCounter, approve, reason) +} + +// CsLPCInterface_ApproveOrDenyConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApproveOrDenyConsumptionLimit' +type CsLPCInterface_ApproveOrDenyConsumptionLimit_Call struct { + *mock.Call +} + +// ApproveOrDenyConsumptionLimit is a helper method to define mock.On call +// - msgCounter model.MsgCounterType +// - approve bool +// - reason string +func (_e *CsLPCInterface_Expecter) ApproveOrDenyConsumptionLimit(msgCounter interface{}, approve interface{}, reason interface{}) *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call { + return &CsLPCInterface_ApproveOrDenyConsumptionLimit_Call{Call: _e.mock.On("ApproveOrDenyConsumptionLimit", msgCounter, approve, reason)} +} + +func (_c *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call) Run(run func(msgCounter model.MsgCounterType, approve bool, reason string)) *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(model.MsgCounterType), args[1].(bool), args[2].(string)) + }) + return _c +} + +func (_c *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call) Return() *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call) RunAndReturn(run func(model.MsgCounterType, bool, string)) *CsLPCInterface_ApproveOrDenyConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// ConsumptionLimit provides a mock function with given fields: +func (_m *CsLPCInterface) ConsumptionLimit() (api.LoadLimit, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ConsumptionLimit") + } + + var r0 api.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func() (api.LoadLimit, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() api.LoadLimit); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(api.LoadLimit) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CsLPCInterface_ConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumptionLimit' +type CsLPCInterface_ConsumptionLimit_Call struct { + *mock.Call +} + +// ConsumptionLimit is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) ConsumptionLimit() *CsLPCInterface_ConsumptionLimit_Call { + return &CsLPCInterface_ConsumptionLimit_Call{Call: _e.mock.On("ConsumptionLimit")} +} + +func (_c *CsLPCInterface_ConsumptionLimit_Call) Run(run func()) *CsLPCInterface_ConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_ConsumptionLimit_Call) Return(_a0 api.LoadLimit, _a1 error) *CsLPCInterface_ConsumptionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CsLPCInterface_ConsumptionLimit_Call) RunAndReturn(run func() (api.LoadLimit, error)) *CsLPCInterface_ConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// ContractualConsumptionNominalMax provides a mock function with given fields: +func (_m *CsLPCInterface) ContractualConsumptionNominalMax() (float64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ContractualConsumptionNominalMax") + } + + 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 +} + +// CsLPCInterface_ContractualConsumptionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContractualConsumptionNominalMax' +type CsLPCInterface_ContractualConsumptionNominalMax_Call struct { + *mock.Call +} + +// ContractualConsumptionNominalMax is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) ContractualConsumptionNominalMax() *CsLPCInterface_ContractualConsumptionNominalMax_Call { + return &CsLPCInterface_ContractualConsumptionNominalMax_Call{Call: _e.mock.On("ContractualConsumptionNominalMax")} +} + +func (_c *CsLPCInterface_ContractualConsumptionNominalMax_Call) Run(run func()) *CsLPCInterface_ContractualConsumptionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_ContractualConsumptionNominalMax_Call) Return(_a0 float64, _a1 error) *CsLPCInterface_ContractualConsumptionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CsLPCInterface_ContractualConsumptionNominalMax_Call) RunAndReturn(run func() (float64, error)) *CsLPCInterface_ContractualConsumptionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeConsumptionActivePowerLimit provides a mock function with given fields: +func (_m *CsLPCInterface) FailsafeConsumptionActivePowerLimit() (float64, bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for FailsafeConsumptionActivePowerLimit") + } + + 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 +} + +// CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeConsumptionActivePowerLimit' +type CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) FailsafeConsumptionActivePowerLimit() *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + return &CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("FailsafeConsumptionActivePowerLimit")} +} + +func (_c *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call) Run(run func()) *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call) Return(value float64, isChangeable bool, resultErr error) *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(value, isChangeable, resultErr) + return _c +} + +func (_c *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func() (float64, bool, error)) *CsLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: +func (_m *CsLPCInterface) 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 +} + +// CsLPCInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type CsLPCInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) FailsafeDurationMinimum() *CsLPCInterface_FailsafeDurationMinimum_Call { + return &CsLPCInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum")} +} + +func (_c *CsLPCInterface_FailsafeDurationMinimum_Call) Run(run func()) *CsLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_FailsafeDurationMinimum_Call) Return(duration time.Duration, isChangeable bool, resultErr error) *CsLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(duration, isChangeable, resultErr) + return _c +} + +func (_c *CsLPCInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func() (time.Duration, bool, error)) *CsLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CsLPCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CsLPCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CsLPCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CsLPCInterface_Expecter) IsCompatibleEntity(entity interface{}) *CsLPCInterface_IsCompatibleEntity_Call { + return &CsLPCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CsLPCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CsLPCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CsLPCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CsLPCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CsLPCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CsLPCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsHeartbeatWithinDuration provides a mock function with given fields: +func (_m *CsLPCInterface) IsHeartbeatWithinDuration() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsHeartbeatWithinDuration") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CsLPCInterface_IsHeartbeatWithinDuration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsHeartbeatWithinDuration' +type CsLPCInterface_IsHeartbeatWithinDuration_Call struct { + *mock.Call +} + +// IsHeartbeatWithinDuration is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) IsHeartbeatWithinDuration() *CsLPCInterface_IsHeartbeatWithinDuration_Call { + return &CsLPCInterface_IsHeartbeatWithinDuration_Call{Call: _e.mock.On("IsHeartbeatWithinDuration")} +} + +func (_c *CsLPCInterface_IsHeartbeatWithinDuration_Call) Run(run func()) *CsLPCInterface_IsHeartbeatWithinDuration_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_IsHeartbeatWithinDuration_Call) Return(_a0 bool) *CsLPCInterface_IsHeartbeatWithinDuration_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CsLPCInterface_IsHeartbeatWithinDuration_Call) RunAndReturn(run func() bool) *CsLPCInterface_IsHeartbeatWithinDuration_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CsLPCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CsLPCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CsLPCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CsLPCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CsLPCInterface_IsUseCaseSupported_Call { + return &CsLPCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CsLPCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CsLPCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CsLPCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CsLPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CsLPCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CsLPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PendingConsumptionLimits provides a mock function with given fields: +func (_m *CsLPCInterface) PendingConsumptionLimits() map[model.MsgCounterType]api.LoadLimit { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for PendingConsumptionLimits") + } + + var r0 map[model.MsgCounterType]api.LoadLimit + if rf, ok := ret.Get(0).(func() map[model.MsgCounterType]api.LoadLimit); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[model.MsgCounterType]api.LoadLimit) + } + } + + return r0 +} + +// CsLPCInterface_PendingConsumptionLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PendingConsumptionLimits' +type CsLPCInterface_PendingConsumptionLimits_Call struct { + *mock.Call +} + +// PendingConsumptionLimits is a helper method to define mock.On call +func (_e *CsLPCInterface_Expecter) PendingConsumptionLimits() *CsLPCInterface_PendingConsumptionLimits_Call { + return &CsLPCInterface_PendingConsumptionLimits_Call{Call: _e.mock.On("PendingConsumptionLimits")} +} + +func (_c *CsLPCInterface_PendingConsumptionLimits_Call) Run(run func()) *CsLPCInterface_PendingConsumptionLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPCInterface_PendingConsumptionLimits_Call) Return(_a0 map[model.MsgCounterType]api.LoadLimit) *CsLPCInterface_PendingConsumptionLimits_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CsLPCInterface_PendingConsumptionLimits_Call) RunAndReturn(run func() map[model.MsgCounterType]api.LoadLimit) *CsLPCInterface_PendingConsumptionLimits_Call { + _c.Call.Return(run) + return _c +} + +// SetConsumptionLimit provides a mock function with given fields: limit +func (_m *CsLPCInterface) SetConsumptionLimit(limit api.LoadLimit) error { + ret := _m.Called(limit) + + if len(ret) == 0 { + panic("no return value specified for SetConsumptionLimit") + } + + var r0 error + if rf, ok := ret.Get(0).(func(api.LoadLimit) error); ok { + r0 = rf(limit) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CsLPCInterface_SetConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetConsumptionLimit' +type CsLPCInterface_SetConsumptionLimit_Call struct { + *mock.Call +} + +// SetConsumptionLimit is a helper method to define mock.On call +// - limit api.LoadLimit +func (_e *CsLPCInterface_Expecter) SetConsumptionLimit(limit interface{}) *CsLPCInterface_SetConsumptionLimit_Call { + return &CsLPCInterface_SetConsumptionLimit_Call{Call: _e.mock.On("SetConsumptionLimit", limit)} +} + +func (_c *CsLPCInterface_SetConsumptionLimit_Call) Run(run func(limit api.LoadLimit)) *CsLPCInterface_SetConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.LoadLimit)) + }) + return _c +} + +func (_c *CsLPCInterface_SetConsumptionLimit_Call) Return(resultErr error) *CsLPCInterface_SetConsumptionLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPCInterface_SetConsumptionLimit_Call) RunAndReturn(run func(api.LoadLimit) error) *CsLPCInterface_SetConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetContractualConsumptionNominalMax provides a mock function with given fields: value +func (_m *CsLPCInterface) SetContractualConsumptionNominalMax(value float64) error { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for SetContractualConsumptionNominalMax") + } + + var r0 error + if rf, ok := ret.Get(0).(func(float64) error); ok { + r0 = rf(value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CsLPCInterface_SetContractualConsumptionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetContractualConsumptionNominalMax' +type CsLPCInterface_SetContractualConsumptionNominalMax_Call struct { + *mock.Call +} + +// SetContractualConsumptionNominalMax is a helper method to define mock.On call +// - value float64 +func (_e *CsLPCInterface_Expecter) SetContractualConsumptionNominalMax(value interface{}) *CsLPCInterface_SetContractualConsumptionNominalMax_Call { + return &CsLPCInterface_SetContractualConsumptionNominalMax_Call{Call: _e.mock.On("SetContractualConsumptionNominalMax", value)} +} + +func (_c *CsLPCInterface_SetContractualConsumptionNominalMax_Call) Run(run func(value float64)) *CsLPCInterface_SetContractualConsumptionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64)) + }) + return _c +} + +func (_c *CsLPCInterface_SetContractualConsumptionNominalMax_Call) Return(resultErr error) *CsLPCInterface_SetContractualConsumptionNominalMax_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPCInterface_SetContractualConsumptionNominalMax_Call) RunAndReturn(run func(float64) error) *CsLPCInterface_SetContractualConsumptionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeConsumptionActivePowerLimit provides a mock function with given fields: value, changeable +func (_m *CsLPCInterface) SetFailsafeConsumptionActivePowerLimit(value float64, changeable bool) error { + ret := _m.Called(value, changeable) + + if len(ret) == 0 { + panic("no return value specified for SetFailsafeConsumptionActivePowerLimit") + } + + 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 +} + +// CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeConsumptionActivePowerLimit' +type CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// SetFailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +// - value float64 +// - changeable bool +func (_e *CsLPCInterface_Expecter) SetFailsafeConsumptionActivePowerLimit(value interface{}, changeable interface{}) *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call { + return &CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("SetFailsafeConsumptionActivePowerLimit", value, changeable)} +} + +func (_c *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call) Run(run func(value float64, changeable bool)) *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64), args[1].(bool)) + }) + return _c +} + +func (_c *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call) Return(resultErr error) *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func(float64, bool) error) *CsLPCInterface_SetFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeDurationMinimum provides a mock function with given fields: duration, changeable +func (_m *CsLPCInterface) 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 +} + +// CsLPCInterface_SetFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeDurationMinimum' +type CsLPCInterface_SetFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// SetFailsafeDurationMinimum is a helper method to define mock.On call +// - duration time.Duration +// - changeable bool +func (_e *CsLPCInterface_Expecter) SetFailsafeDurationMinimum(duration interface{}, changeable interface{}) *CsLPCInterface_SetFailsafeDurationMinimum_Call { + return &CsLPCInterface_SetFailsafeDurationMinimum_Call{Call: _e.mock.On("SetFailsafeDurationMinimum", duration, changeable)} +} + +func (_c *CsLPCInterface_SetFailsafeDurationMinimum_Call) Run(run func(duration time.Duration, changeable bool)) *CsLPCInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration), args[1].(bool)) + }) + return _c +} + +func (_c *CsLPCInterface_SetFailsafeDurationMinimum_Call) Return(resultErr error) *CsLPCInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPCInterface_SetFailsafeDurationMinimum_Call) RunAndReturn(run func(time.Duration, bool) error) *CsLPCInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CsLPCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CsLPCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CsLPCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CsLPCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CsLPCInterface_UpdateUseCaseAvailability_Call { + return &CsLPCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CsLPCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CsLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CsLPCInterface_UpdateUseCaseAvailability_Call) Return() *CsLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CsLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCsLPCInterface creates a new instance of CsLPCInterface. 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 NewCsLPCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CsLPCInterface { + mock := &CsLPCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/CsLPPInterface.go b/usecases/mocks/CsLPPInterface.go new file mode 100644 index 00000000..f3ab99e3 --- /dev/null +++ b/usecases/mocks/CsLPPInterface.go @@ -0,0 +1,787 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" + + time "time" +) + +// CsLPPInterface is an autogenerated mock type for the CsLPPInterface type +type CsLPPInterface struct { + mock.Mock +} + +type CsLPPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CsLPPInterface) EXPECT() *CsLPPInterface_Expecter { + return &CsLPPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *CsLPPInterface) AddFeatures() { + _m.Called() +} + +// CsLPPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CsLPPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) AddFeatures() *CsLPPInterface_AddFeatures_Call { + return &CsLPPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CsLPPInterface_AddFeatures_Call) Run(run func()) *CsLPPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_AddFeatures_Call) Return() *CsLPPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPPInterface_AddFeatures_Call) RunAndReturn(run func()) *CsLPPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *CsLPPInterface) AddUseCase() { + _m.Called() +} + +// CsLPPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CsLPPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) AddUseCase() *CsLPPInterface_AddUseCase_Call { + return &CsLPPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CsLPPInterface_AddUseCase_Call) Run(run func()) *CsLPPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_AddUseCase_Call) Return() *CsLPPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPPInterface_AddUseCase_Call) RunAndReturn(run func()) *CsLPPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ApproveOrDenyProductionLimit provides a mock function with given fields: msgCounter, approve, reason +func (_m *CsLPPInterface) ApproveOrDenyProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + _m.Called(msgCounter, approve, reason) +} + +// CsLPPInterface_ApproveOrDenyProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApproveOrDenyProductionLimit' +type CsLPPInterface_ApproveOrDenyProductionLimit_Call struct { + *mock.Call +} + +// ApproveOrDenyProductionLimit is a helper method to define mock.On call +// - msgCounter model.MsgCounterType +// - approve bool +// - reason string +func (_e *CsLPPInterface_Expecter) ApproveOrDenyProductionLimit(msgCounter interface{}, approve interface{}, reason interface{}) *CsLPPInterface_ApproveOrDenyProductionLimit_Call { + return &CsLPPInterface_ApproveOrDenyProductionLimit_Call{Call: _e.mock.On("ApproveOrDenyProductionLimit", msgCounter, approve, reason)} +} + +func (_c *CsLPPInterface_ApproveOrDenyProductionLimit_Call) Run(run func(msgCounter model.MsgCounterType, approve bool, reason string)) *CsLPPInterface_ApproveOrDenyProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(model.MsgCounterType), args[1].(bool), args[2].(string)) + }) + return _c +} + +func (_c *CsLPPInterface_ApproveOrDenyProductionLimit_Call) Return() *CsLPPInterface_ApproveOrDenyProductionLimit_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPPInterface_ApproveOrDenyProductionLimit_Call) RunAndReturn(run func(model.MsgCounterType, bool, string)) *CsLPPInterface_ApproveOrDenyProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// ContractualProductionNominalMax provides a mock function with given fields: +func (_m *CsLPPInterface) 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 +} + +// CsLPPInterface_ContractualProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ContractualProductionNominalMax' +type CsLPPInterface_ContractualProductionNominalMax_Call struct { + *mock.Call +} + +// ContractualProductionNominalMax is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) ContractualProductionNominalMax() *CsLPPInterface_ContractualProductionNominalMax_Call { + return &CsLPPInterface_ContractualProductionNominalMax_Call{Call: _e.mock.On("ContractualProductionNominalMax")} +} + +func (_c *CsLPPInterface_ContractualProductionNominalMax_Call) Run(run func()) *CsLPPInterface_ContractualProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_ContractualProductionNominalMax_Call) Return(_a0 float64, _a1 error) *CsLPPInterface_ContractualProductionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CsLPPInterface_ContractualProductionNominalMax_Call) RunAndReturn(run func() (float64, error)) *CsLPPInterface_ContractualProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: +func (_m *CsLPPInterface) 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 +} + +// CsLPPInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type CsLPPInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) FailsafeDurationMinimum() *CsLPPInterface_FailsafeDurationMinimum_Call { + return &CsLPPInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum")} +} + +func (_c *CsLPPInterface_FailsafeDurationMinimum_Call) Run(run func()) *CsLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_FailsafeDurationMinimum_Call) Return(duration time.Duration, isChangeable bool, resultErr error) *CsLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(duration, isChangeable, resultErr) + return _c +} + +func (_c *CsLPPInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func() (time.Duration, bool, error)) *CsLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeProductionActivePowerLimit provides a mock function with given fields: +func (_m *CsLPPInterface) 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 +} + +// CsLPPInterface_FailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeProductionActivePowerLimit' +type CsLPPInterface_FailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeProductionActivePowerLimit is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) FailsafeProductionActivePowerLimit() *CsLPPInterface_FailsafeProductionActivePowerLimit_Call { + return &CsLPPInterface_FailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("FailsafeProductionActivePowerLimit")} +} + +func (_c *CsLPPInterface_FailsafeProductionActivePowerLimit_Call) Run(run func()) *CsLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_FailsafeProductionActivePowerLimit_Call) Return(value float64, isChangeable bool, resultErr error) *CsLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(value, isChangeable, resultErr) + return _c +} + +func (_c *CsLPPInterface_FailsafeProductionActivePowerLimit_Call) RunAndReturn(run func() (float64, bool, error)) *CsLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *CsLPPInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CsLPPInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type CsLPPInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *CsLPPInterface_Expecter) IsCompatibleEntity(entity interface{}) *CsLPPInterface_IsCompatibleEntity_Call { + return &CsLPPInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *CsLPPInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *CsLPPInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CsLPPInterface_IsCompatibleEntity_Call) Return(_a0 bool) *CsLPPInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CsLPPInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *CsLPPInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsHeartbeatWithinDuration provides a mock function with given fields: +func (_m *CsLPPInterface) IsHeartbeatWithinDuration() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsHeartbeatWithinDuration") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// CsLPPInterface_IsHeartbeatWithinDuration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsHeartbeatWithinDuration' +type CsLPPInterface_IsHeartbeatWithinDuration_Call struct { + *mock.Call +} + +// IsHeartbeatWithinDuration is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) IsHeartbeatWithinDuration() *CsLPPInterface_IsHeartbeatWithinDuration_Call { + return &CsLPPInterface_IsHeartbeatWithinDuration_Call{Call: _e.mock.On("IsHeartbeatWithinDuration")} +} + +func (_c *CsLPPInterface_IsHeartbeatWithinDuration_Call) Run(run func()) *CsLPPInterface_IsHeartbeatWithinDuration_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_IsHeartbeatWithinDuration_Call) Return(_a0 bool) *CsLPPInterface_IsHeartbeatWithinDuration_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CsLPPInterface_IsHeartbeatWithinDuration_Call) RunAndReturn(run func() bool) *CsLPPInterface_IsHeartbeatWithinDuration_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *CsLPPInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CsLPPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type CsLPPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *CsLPPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *CsLPPInterface_IsUseCaseSupported_Call { + return &CsLPPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *CsLPPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *CsLPPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *CsLPPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *CsLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CsLPPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *CsLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PendingProductionLimits provides a mock function with given fields: +func (_m *CsLPPInterface) PendingProductionLimits() map[model.MsgCounterType]api.LoadLimit { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for PendingProductionLimits") + } + + var r0 map[model.MsgCounterType]api.LoadLimit + if rf, ok := ret.Get(0).(func() map[model.MsgCounterType]api.LoadLimit); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[model.MsgCounterType]api.LoadLimit) + } + } + + return r0 +} + +// CsLPPInterface_PendingProductionLimits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PendingProductionLimits' +type CsLPPInterface_PendingProductionLimits_Call struct { + *mock.Call +} + +// PendingProductionLimits is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) PendingProductionLimits() *CsLPPInterface_PendingProductionLimits_Call { + return &CsLPPInterface_PendingProductionLimits_Call{Call: _e.mock.On("PendingProductionLimits")} +} + +func (_c *CsLPPInterface_PendingProductionLimits_Call) Run(run func()) *CsLPPInterface_PendingProductionLimits_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_PendingProductionLimits_Call) Return(_a0 map[model.MsgCounterType]api.LoadLimit) *CsLPPInterface_PendingProductionLimits_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *CsLPPInterface_PendingProductionLimits_Call) RunAndReturn(run func() map[model.MsgCounterType]api.LoadLimit) *CsLPPInterface_PendingProductionLimits_Call { + _c.Call.Return(run) + return _c +} + +// ProductionLimit provides a mock function with given fields: +func (_m *CsLPPInterface) ProductionLimit() (api.LoadLimit, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ProductionLimit") + } + + var r0 api.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func() (api.LoadLimit, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() api.LoadLimit); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(api.LoadLimit) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CsLPPInterface_ProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProductionLimit' +type CsLPPInterface_ProductionLimit_Call struct { + *mock.Call +} + +// ProductionLimit is a helper method to define mock.On call +func (_e *CsLPPInterface_Expecter) ProductionLimit() *CsLPPInterface_ProductionLimit_Call { + return &CsLPPInterface_ProductionLimit_Call{Call: _e.mock.On("ProductionLimit")} +} + +func (_c *CsLPPInterface_ProductionLimit_Call) Run(run func()) *CsLPPInterface_ProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CsLPPInterface_ProductionLimit_Call) Return(_a0 api.LoadLimit, _a1 error) *CsLPPInterface_ProductionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CsLPPInterface_ProductionLimit_Call) RunAndReturn(run func() (api.LoadLimit, error)) *CsLPPInterface_ProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetContractualProductionNominalMax provides a mock function with given fields: value +func (_m *CsLPPInterface) 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 +} + +// CsLPPInterface_SetContractualProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetContractualProductionNominalMax' +type CsLPPInterface_SetContractualProductionNominalMax_Call struct { + *mock.Call +} + +// SetContractualProductionNominalMax is a helper method to define mock.On call +// - value float64 +func (_e *CsLPPInterface_Expecter) SetContractualProductionNominalMax(value interface{}) *CsLPPInterface_SetContractualProductionNominalMax_Call { + return &CsLPPInterface_SetContractualProductionNominalMax_Call{Call: _e.mock.On("SetContractualProductionNominalMax", value)} +} + +func (_c *CsLPPInterface_SetContractualProductionNominalMax_Call) Run(run func(value float64)) *CsLPPInterface_SetContractualProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64)) + }) + return _c +} + +func (_c *CsLPPInterface_SetContractualProductionNominalMax_Call) Return(resultErr error) *CsLPPInterface_SetContractualProductionNominalMax_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPPInterface_SetContractualProductionNominalMax_Call) RunAndReturn(run func(float64) error) *CsLPPInterface_SetContractualProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeDurationMinimum provides a mock function with given fields: duration, changeable +func (_m *CsLPPInterface) 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 +} + +// CsLPPInterface_SetFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeDurationMinimum' +type CsLPPInterface_SetFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// SetFailsafeDurationMinimum is a helper method to define mock.On call +// - duration time.Duration +// - changeable bool +func (_e *CsLPPInterface_Expecter) SetFailsafeDurationMinimum(duration interface{}, changeable interface{}) *CsLPPInterface_SetFailsafeDurationMinimum_Call { + return &CsLPPInterface_SetFailsafeDurationMinimum_Call{Call: _e.mock.On("SetFailsafeDurationMinimum", duration, changeable)} +} + +func (_c *CsLPPInterface_SetFailsafeDurationMinimum_Call) Run(run func(duration time.Duration, changeable bool)) *CsLPPInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration), args[1].(bool)) + }) + return _c +} + +func (_c *CsLPPInterface_SetFailsafeDurationMinimum_Call) Return(resultErr error) *CsLPPInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPPInterface_SetFailsafeDurationMinimum_Call) RunAndReturn(run func(time.Duration, bool) error) *CsLPPInterface_SetFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// SetFailsafeProductionActivePowerLimit provides a mock function with given fields: value, changeable +func (_m *CsLPPInterface) 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 +} + +// CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFailsafeProductionActivePowerLimit' +type CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// SetFailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - value float64 +// - changeable bool +func (_e *CsLPPInterface_Expecter) SetFailsafeProductionActivePowerLimit(value interface{}, changeable interface{}) *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call { + return &CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("SetFailsafeProductionActivePowerLimit", value, changeable)} +} + +func (_c *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call) Run(run func(value float64, changeable bool)) *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(float64), args[1].(bool)) + }) + return _c +} + +func (_c *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call) Return(resultErr error) *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(float64, bool) error) *CsLPPInterface_SetFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// SetProductionLimit provides a mock function with given fields: limit +func (_m *CsLPPInterface) SetProductionLimit(limit api.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(api.LoadLimit) error); ok { + r0 = rf(limit) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// CsLPPInterface_SetProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetProductionLimit' +type CsLPPInterface_SetProductionLimit_Call struct { + *mock.Call +} + +// SetProductionLimit is a helper method to define mock.On call +// - limit api.LoadLimit +func (_e *CsLPPInterface_Expecter) SetProductionLimit(limit interface{}) *CsLPPInterface_SetProductionLimit_Call { + return &CsLPPInterface_SetProductionLimit_Call{Call: _e.mock.On("SetProductionLimit", limit)} +} + +func (_c *CsLPPInterface_SetProductionLimit_Call) Run(run func(limit api.LoadLimit)) *CsLPPInterface_SetProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(api.LoadLimit)) + }) + return _c +} + +func (_c *CsLPPInterface_SetProductionLimit_Call) Return(resultErr error) *CsLPPInterface_SetProductionLimit_Call { + _c.Call.Return(resultErr) + return _c +} + +func (_c *CsLPPInterface_SetProductionLimit_Call) RunAndReturn(run func(api.LoadLimit) error) *CsLPPInterface_SetProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *CsLPPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// CsLPPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CsLPPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CsLPPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CsLPPInterface_UpdateUseCaseAvailability_Call { + return &CsLPPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CsLPPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CsLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *CsLPPInterface_UpdateUseCaseAvailability_Call) Return() *CsLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CsLPPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *CsLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// NewCsLPPInterface creates a new instance of CsLPPInterface. 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 NewCsLPPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CsLPPInterface { + mock := &CsLPPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/EgLPCInterface.go b/usecases/mocks/EgLPCInterface.go new file mode 100644 index 00000000..e0450a6d --- /dev/null +++ b/usecases/mocks/EgLPCInterface.go @@ -0,0 +1,641 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" + + time "time" +) + +// EgLPCInterface is an autogenerated mock type for the EgLPCInterface type +type EgLPCInterface struct { + mock.Mock +} + +type EgLPCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *EgLPCInterface) EXPECT() *EgLPCInterface_Expecter { + return &EgLPCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *EgLPCInterface) AddFeatures() { + _m.Called() +} + +// EgLPCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type EgLPCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *EgLPCInterface_Expecter) AddFeatures() *EgLPCInterface_AddFeatures_Call { + return &EgLPCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *EgLPCInterface_AddFeatures_Call) Run(run func()) *EgLPCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *EgLPCInterface_AddFeatures_Call) Return() *EgLPCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *EgLPCInterface_AddFeatures_Call) RunAndReturn(run func()) *EgLPCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *EgLPCInterface) AddUseCase() { + _m.Called() +} + +// EgLPCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type EgLPCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *EgLPCInterface_Expecter) AddUseCase() *EgLPCInterface_AddUseCase_Call { + return &EgLPCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *EgLPCInterface_AddUseCase_Call) Run(run func()) *EgLPCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *EgLPCInterface_AddUseCase_Call) Return() *EgLPCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *EgLPCInterface_AddUseCase_Call) RunAndReturn(run func()) *EgLPCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// ConsumptionLimit provides a mock function with given fields: entity +func (_m *EgLPCInterface) ConsumptionLimit(entity spine_goapi.EntityRemoteInterface) (api.LoadLimit, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ConsumptionLimit") + } + + var r0 api.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.LoadLimit, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.LoadLimit); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(api.LoadLimit) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_ConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConsumptionLimit' +type EgLPCInterface_ConsumptionLimit_Call struct { + *mock.Call +} + +// ConsumptionLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPCInterface_Expecter) ConsumptionLimit(entity interface{}) *EgLPCInterface_ConsumptionLimit_Call { + return &EgLPCInterface_ConsumptionLimit_Call{Call: _e.mock.On("ConsumptionLimit", entity)} +} + +func (_c *EgLPCInterface_ConsumptionLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPCInterface_ConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPCInterface_ConsumptionLimit_Call) Return(limit api.LoadLimit, resultErr error) *EgLPCInterface_ConsumptionLimit_Call { + _c.Call.Return(limit, resultErr) + return _c +} + +func (_c *EgLPCInterface_ConsumptionLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.LoadLimit, error)) *EgLPCInterface_ConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeConsumptionActivePowerLimit provides a mock function with given fields: entity +func (_m *EgLPCInterface) FailsafeConsumptionActivePowerLimit(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for FailsafeConsumptionActivePowerLimit") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeConsumptionActivePowerLimit' +type EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPCInterface_Expecter) FailsafeConsumptionActivePowerLimit(entity interface{}) *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + return &EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("FailsafeConsumptionActivePowerLimit", entity)} +} + +func (_c *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call) Return(_a0 float64, _a1 error) *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *EgLPCInterface_FailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: entity +func (_m *EgLPCInterface) FailsafeDurationMinimum(entity spine_goapi.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(spine_goapi.EntityRemoteInterface) (time.Duration, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) time.Duration); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type EgLPCInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPCInterface_Expecter) FailsafeDurationMinimum(entity interface{}) *EgLPCInterface_FailsafeDurationMinimum_Call { + return &EgLPCInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum", entity)} +} + +func (_c *EgLPCInterface_FailsafeDurationMinimum_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPCInterface_FailsafeDurationMinimum_Call) Return(_a0 time.Duration, _a1 error) *EgLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (time.Duration, error)) *EgLPCInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *EgLPCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// EgLPCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type EgLPCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPCInterface_Expecter) IsCompatibleEntity(entity interface{}) *EgLPCInterface_IsCompatibleEntity_Call { + return &EgLPCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *EgLPCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *EgLPCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *EgLPCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *EgLPCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *EgLPCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type EgLPCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *EgLPCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *EgLPCInterface_IsUseCaseSupported_Call { + return &EgLPCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *EgLPCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *EgLPCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *EgLPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *EgLPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PowerConsumptionNominalMax provides a mock function with given fields: entity +func (_m *EgLPCInterface) PowerConsumptionNominalMax(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerConsumptionNominalMax") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_PowerConsumptionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerConsumptionNominalMax' +type EgLPCInterface_PowerConsumptionNominalMax_Call struct { + *mock.Call +} + +// PowerConsumptionNominalMax is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPCInterface_Expecter) PowerConsumptionNominalMax(entity interface{}) *EgLPCInterface_PowerConsumptionNominalMax_Call { + return &EgLPCInterface_PowerConsumptionNominalMax_Call{Call: _e.mock.On("PowerConsumptionNominalMax", entity)} +} + +func (_c *EgLPCInterface_PowerConsumptionNominalMax_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPCInterface_PowerConsumptionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPCInterface_PowerConsumptionNominalMax_Call) Return(_a0 float64, _a1 error) *EgLPCInterface_PowerConsumptionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_PowerConsumptionNominalMax_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *EgLPCInterface_PowerConsumptionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *EgLPCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// EgLPCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type EgLPCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *EgLPCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *EgLPCInterface_UpdateUseCaseAvailability_Call { + return &EgLPCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *EgLPCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *EgLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *EgLPCInterface_UpdateUseCaseAvailability_Call) Return() *EgLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *EgLPCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *EgLPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// WriteConsumptionLimit provides a mock function with given fields: entity, limit +func (_m *EgLPCInterface) WriteConsumptionLimit(entity spine_goapi.EntityRemoteInterface, limit api.LoadLimit) (*model.MsgCounterType, error) { + ret := _m.Called(entity, limit) + + if len(ret) == 0 { + panic("no return value specified for WriteConsumptionLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, api.LoadLimit) (*model.MsgCounterType, error)); ok { + return rf(entity, limit) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, api.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(spine_goapi.EntityRemoteInterface, api.LoadLimit) error); ok { + r1 = rf(entity, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_WriteConsumptionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteConsumptionLimit' +type EgLPCInterface_WriteConsumptionLimit_Call struct { + *mock.Call +} + +// WriteConsumptionLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - limit api.LoadLimit +func (_e *EgLPCInterface_Expecter) WriteConsumptionLimit(entity interface{}, limit interface{}) *EgLPCInterface_WriteConsumptionLimit_Call { + return &EgLPCInterface_WriteConsumptionLimit_Call{Call: _e.mock.On("WriteConsumptionLimit", entity, limit)} +} + +func (_c *EgLPCInterface_WriteConsumptionLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, limit api.LoadLimit)) *EgLPCInterface_WriteConsumptionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].(api.LoadLimit)) + }) + return _c +} + +func (_c *EgLPCInterface_WriteConsumptionLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *EgLPCInterface_WriteConsumptionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_WriteConsumptionLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, api.LoadLimit) (*model.MsgCounterType, error)) *EgLPCInterface_WriteConsumptionLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeConsumptionActivePowerLimit provides a mock function with given fields: entity, value +func (_m *EgLPCInterface) WriteFailsafeConsumptionActivePowerLimit(entity spine_goapi.EntityRemoteInterface, value float64) (*model.MsgCounterType, error) { + ret := _m.Called(entity, value) + + if len(ret) == 0 { + panic("no return value specified for WriteFailsafeConsumptionActivePowerLimit") + } + + var r0 *model.MsgCounterType + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, float64) (*model.MsgCounterType, error)); ok { + return rf(entity, value) + } + if rf, ok := ret.Get(0).(func(spine_goapi.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(spine_goapi.EntityRemoteInterface, float64) error); ok { + r1 = rf(entity, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeConsumptionActivePowerLimit' +type EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call struct { + *mock.Call +} + +// WriteFailsafeConsumptionActivePowerLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - value float64 +func (_e *EgLPCInterface_Expecter) WriteFailsafeConsumptionActivePowerLimit(entity interface{}, value interface{}) *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + return &EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call{Call: _e.mock.On("WriteFailsafeConsumptionActivePowerLimit", entity, value)} +} + +func (_c *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, value float64)) *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].(float64)) + }) + return _c +} + +func (_c *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, float64) (*model.MsgCounterType, error)) *EgLPCInterface_WriteFailsafeConsumptionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeDurationMinimum provides a mock function with given fields: entity, duration +func (_m *EgLPCInterface) WriteFailsafeDurationMinimum(entity spine_goapi.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(spine_goapi.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)); ok { + return rf(entity, duration) + } + if rf, ok := ret.Get(0).(func(spine_goapi.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(spine_goapi.EntityRemoteInterface, time.Duration) error); ok { + r1 = rf(entity, duration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPCInterface_WriteFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeDurationMinimum' +type EgLPCInterface_WriteFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// WriteFailsafeDurationMinimum is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - duration time.Duration +func (_e *EgLPCInterface_Expecter) WriteFailsafeDurationMinimum(entity interface{}, duration interface{}) *EgLPCInterface_WriteFailsafeDurationMinimum_Call { + return &EgLPCInterface_WriteFailsafeDurationMinimum_Call{Call: _e.mock.On("WriteFailsafeDurationMinimum", entity, duration)} +} + +func (_c *EgLPCInterface_WriteFailsafeDurationMinimum_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, duration time.Duration)) *EgLPCInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].(time.Duration)) + }) + return _c +} + +func (_c *EgLPCInterface_WriteFailsafeDurationMinimum_Call) Return(_a0 *model.MsgCounterType, _a1 error) *EgLPCInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPCInterface_WriteFailsafeDurationMinimum_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)) *EgLPCInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// NewEgLPCInterface creates a new instance of EgLPCInterface. 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 NewEgLPCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *EgLPCInterface { + mock := &EgLPCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/EgLPPInterface.go b/usecases/mocks/EgLPPInterface.go new file mode 100644 index 00000000..be6d4f2f --- /dev/null +++ b/usecases/mocks/EgLPPInterface.go @@ -0,0 +1,641 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + api "github.com/enbility/eebus-go/usecases/api" + mock "github.com/stretchr/testify/mock" + + model "github.com/enbility/spine-go/model" + + spine_goapi "github.com/enbility/spine-go/api" + + time "time" +) + +// EgLPPInterface is an autogenerated mock type for the EgLPPInterface type +type EgLPPInterface struct { + mock.Mock +} + +type EgLPPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *EgLPPInterface) EXPECT() *EgLPPInterface_Expecter { + return &EgLPPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *EgLPPInterface) AddFeatures() { + _m.Called() +} + +// EgLPPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type EgLPPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *EgLPPInterface_Expecter) AddFeatures() *EgLPPInterface_AddFeatures_Call { + return &EgLPPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *EgLPPInterface_AddFeatures_Call) Run(run func()) *EgLPPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *EgLPPInterface_AddFeatures_Call) Return() *EgLPPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *EgLPPInterface_AddFeatures_Call) RunAndReturn(run func()) *EgLPPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *EgLPPInterface) AddUseCase() { + _m.Called() +} + +// EgLPPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type EgLPPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *EgLPPInterface_Expecter) AddUseCase() *EgLPPInterface_AddUseCase_Call { + return &EgLPPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *EgLPPInterface_AddUseCase_Call) Run(run func()) *EgLPPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *EgLPPInterface_AddUseCase_Call) Return() *EgLPPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *EgLPPInterface_AddUseCase_Call) RunAndReturn(run func()) *EgLPPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeDurationMinimum provides a mock function with given fields: entity +func (_m *EgLPPInterface) FailsafeDurationMinimum(entity spine_goapi.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(spine_goapi.EntityRemoteInterface) (time.Duration, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) time.Duration); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(time.Duration) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_FailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeDurationMinimum' +type EgLPPInterface_FailsafeDurationMinimum_Call struct { + *mock.Call +} + +// FailsafeDurationMinimum is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPPInterface_Expecter) FailsafeDurationMinimum(entity interface{}) *EgLPPInterface_FailsafeDurationMinimum_Call { + return &EgLPPInterface_FailsafeDurationMinimum_Call{Call: _e.mock.On("FailsafeDurationMinimum", entity)} +} + +func (_c *EgLPPInterface_FailsafeDurationMinimum_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPPInterface_FailsafeDurationMinimum_Call) Return(_a0 time.Duration, _a1 error) *EgLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_FailsafeDurationMinimum_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (time.Duration, error)) *EgLPPInterface_FailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// FailsafeProductionActivePowerLimit provides a mock function with given fields: entity +func (_m *EgLPPInterface) FailsafeProductionActivePowerLimit(entity spine_goapi.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(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_FailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FailsafeProductionActivePowerLimit' +type EgLPPInterface_FailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// FailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPPInterface_Expecter) FailsafeProductionActivePowerLimit(entity interface{}) *EgLPPInterface_FailsafeProductionActivePowerLimit_Call { + return &EgLPPInterface_FailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("FailsafeProductionActivePowerLimit", entity)} +} + +func (_c *EgLPPInterface_FailsafeProductionActivePowerLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPPInterface_FailsafeProductionActivePowerLimit_Call) Return(_a0 float64, _a1 error) *EgLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_FailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *EgLPPInterface_FailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *EgLPPInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// EgLPPInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type EgLPPInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPPInterface_Expecter) IsCompatibleEntity(entity interface{}) *EgLPPInterface_IsCompatibleEntity_Call { + return &EgLPPInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *EgLPPInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPPInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPPInterface_IsCompatibleEntity_Call) Return(_a0 bool) *EgLPPInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *EgLPPInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *EgLPPInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *EgLPPInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type EgLPPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *EgLPPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *EgLPPInterface_IsUseCaseSupported_Call { + return &EgLPPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *EgLPPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *EgLPPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *EgLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *EgLPPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// PowerProductionNominalMax provides a mock function with given fields: entity +func (_m *EgLPPInterface) PowerProductionNominalMax(entity spine_goapi.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(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_PowerProductionNominalMax_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerProductionNominalMax' +type EgLPPInterface_PowerProductionNominalMax_Call struct { + *mock.Call +} + +// PowerProductionNominalMax is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPPInterface_Expecter) PowerProductionNominalMax(entity interface{}) *EgLPPInterface_PowerProductionNominalMax_Call { + return &EgLPPInterface_PowerProductionNominalMax_Call{Call: _e.mock.On("PowerProductionNominalMax", entity)} +} + +func (_c *EgLPPInterface_PowerProductionNominalMax_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPPInterface_PowerProductionNominalMax_Call) Return(_a0 float64, _a1 error) *EgLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_PowerProductionNominalMax_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *EgLPPInterface_PowerProductionNominalMax_Call { + _c.Call.Return(run) + return _c +} + +// ProductionLimit provides a mock function with given fields: entity +func (_m *EgLPPInterface) ProductionLimit(entity spine_goapi.EntityRemoteInterface) (api.LoadLimit, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for ProductionLimit") + } + + var r0 api.LoadLimit + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (api.LoadLimit, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) api.LoadLimit); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(api.LoadLimit) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_ProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProductionLimit' +type EgLPPInterface_ProductionLimit_Call struct { + *mock.Call +} + +// ProductionLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *EgLPPInterface_Expecter) ProductionLimit(entity interface{}) *EgLPPInterface_ProductionLimit_Call { + return &EgLPPInterface_ProductionLimit_Call{Call: _e.mock.On("ProductionLimit", entity)} +} + +func (_c *EgLPPInterface_ProductionLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *EgLPPInterface_ProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *EgLPPInterface_ProductionLimit_Call) Return(limit api.LoadLimit, resultErr error) *EgLPPInterface_ProductionLimit_Call { + _c.Call.Return(limit, resultErr) + return _c +} + +func (_c *EgLPPInterface_ProductionLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (api.LoadLimit, error)) *EgLPPInterface_ProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *EgLPPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// EgLPPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type EgLPPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *EgLPPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *EgLPPInterface_UpdateUseCaseAvailability_Call { + return &EgLPPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *EgLPPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *EgLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *EgLPPInterface_UpdateUseCaseAvailability_Call) Return() *EgLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *EgLPPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *EgLPPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeDurationMinimum provides a mock function with given fields: entity, duration +func (_m *EgLPPInterface) WriteFailsafeDurationMinimum(entity spine_goapi.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(spine_goapi.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)); ok { + return rf(entity, duration) + } + if rf, ok := ret.Get(0).(func(spine_goapi.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(spine_goapi.EntityRemoteInterface, time.Duration) error); ok { + r1 = rf(entity, duration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_WriteFailsafeDurationMinimum_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeDurationMinimum' +type EgLPPInterface_WriteFailsafeDurationMinimum_Call struct { + *mock.Call +} + +// WriteFailsafeDurationMinimum is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - duration time.Duration +func (_e *EgLPPInterface_Expecter) WriteFailsafeDurationMinimum(entity interface{}, duration interface{}) *EgLPPInterface_WriteFailsafeDurationMinimum_Call { + return &EgLPPInterface_WriteFailsafeDurationMinimum_Call{Call: _e.mock.On("WriteFailsafeDurationMinimum", entity, duration)} +} + +func (_c *EgLPPInterface_WriteFailsafeDurationMinimum_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, duration time.Duration)) *EgLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].(time.Duration)) + }) + return _c +} + +func (_c *EgLPPInterface_WriteFailsafeDurationMinimum_Call) Return(_a0 *model.MsgCounterType, _a1 error) *EgLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_WriteFailsafeDurationMinimum_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, time.Duration) (*model.MsgCounterType, error)) *EgLPPInterface_WriteFailsafeDurationMinimum_Call { + _c.Call.Return(run) + return _c +} + +// WriteFailsafeProductionActivePowerLimit provides a mock function with given fields: entity, value +func (_m *EgLPPInterface) WriteFailsafeProductionActivePowerLimit(entity spine_goapi.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(spine_goapi.EntityRemoteInterface, float64) (*model.MsgCounterType, error)); ok { + return rf(entity, value) + } + if rf, ok := ret.Get(0).(func(spine_goapi.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(spine_goapi.EntityRemoteInterface, float64) error); ok { + r1 = rf(entity, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteFailsafeProductionActivePowerLimit' +type EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call struct { + *mock.Call +} + +// WriteFailsafeProductionActivePowerLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - value float64 +func (_e *EgLPPInterface_Expecter) WriteFailsafeProductionActivePowerLimit(entity interface{}, value interface{}) *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + return &EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call{Call: _e.mock.On("WriteFailsafeProductionActivePowerLimit", entity, value)} +} + +func (_c *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, value float64)) *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].(float64)) + }) + return _c +} + +func (_c *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, float64) (*model.MsgCounterType, error)) *EgLPPInterface_WriteFailsafeProductionActivePowerLimit_Call { + _c.Call.Return(run) + return _c +} + +// WriteProductionLimit provides a mock function with given fields: entity, limit +func (_m *EgLPPInterface) WriteProductionLimit(entity spine_goapi.EntityRemoteInterface, limit api.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(spine_goapi.EntityRemoteInterface, api.LoadLimit) (*model.MsgCounterType, error)); ok { + return rf(entity, limit) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface, api.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(spine_goapi.EntityRemoteInterface, api.LoadLimit) error); ok { + r1 = rf(entity, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EgLPPInterface_WriteProductionLimit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteProductionLimit' +type EgLPPInterface_WriteProductionLimit_Call struct { + *mock.Call +} + +// WriteProductionLimit is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +// - limit api.LoadLimit +func (_e *EgLPPInterface_Expecter) WriteProductionLimit(entity interface{}, limit interface{}) *EgLPPInterface_WriteProductionLimit_Call { + return &EgLPPInterface_WriteProductionLimit_Call{Call: _e.mock.On("WriteProductionLimit", entity, limit)} +} + +func (_c *EgLPPInterface_WriteProductionLimit_Call) Run(run func(entity spine_goapi.EntityRemoteInterface, limit api.LoadLimit)) *EgLPPInterface_WriteProductionLimit_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface), args[1].(api.LoadLimit)) + }) + return _c +} + +func (_c *EgLPPInterface_WriteProductionLimit_Call) Return(_a0 *model.MsgCounterType, _a1 error) *EgLPPInterface_WriteProductionLimit_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *EgLPPInterface_WriteProductionLimit_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface, api.LoadLimit) (*model.MsgCounterType, error)) *EgLPPInterface_WriteProductionLimit_Call { + _c.Call.Return(run) + return _c +} + +// NewEgLPPInterface creates a new instance of EgLPPInterface. 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 NewEgLPPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *EgLPPInterface { + mock := &EgLPPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/GcpMGCPInterface.go b/usecases/mocks/GcpMGCPInterface.go new file mode 100644 index 00000000..03bbe15c --- /dev/null +++ b/usecases/mocks/GcpMGCPInterface.go @@ -0,0 +1,630 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// GcpMGCPInterface is an autogenerated mock type for the GcpMGCPInterface type +type GcpMGCPInterface struct { + mock.Mock +} + +type GcpMGCPInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *GcpMGCPInterface) EXPECT() *GcpMGCPInterface_Expecter { + return &GcpMGCPInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *GcpMGCPInterface) AddFeatures() { + _m.Called() +} + +// GcpMGCPInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type GcpMGCPInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *GcpMGCPInterface_Expecter) AddFeatures() *GcpMGCPInterface_AddFeatures_Call { + return &GcpMGCPInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *GcpMGCPInterface_AddFeatures_Call) Run(run func()) *GcpMGCPInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GcpMGCPInterface_AddFeatures_Call) Return() *GcpMGCPInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *GcpMGCPInterface_AddFeatures_Call) RunAndReturn(run func()) *GcpMGCPInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *GcpMGCPInterface) AddUseCase() { + _m.Called() +} + +// GcpMGCPInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type GcpMGCPInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *GcpMGCPInterface_Expecter) AddUseCase() *GcpMGCPInterface_AddUseCase_Call { + return &GcpMGCPInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *GcpMGCPInterface_AddUseCase_Call) Run(run func()) *GcpMGCPInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GcpMGCPInterface_AddUseCase_Call) Return() *GcpMGCPInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *GcpMGCPInterface_AddUseCase_Call) RunAndReturn(run func()) *GcpMGCPInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentPerPhase provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) CurrentPerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_CurrentPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentPerPhase' +type GcpMGCPInterface_CurrentPerPhase_Call struct { + *mock.Call +} + +// CurrentPerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) CurrentPerPhase(entity interface{}) *GcpMGCPInterface_CurrentPerPhase_Call { + return &GcpMGCPInterface_CurrentPerPhase_Call{Call: _e.mock.On("CurrentPerPhase", entity)} +} + +func (_c *GcpMGCPInterface_CurrentPerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_CurrentPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_CurrentPerPhase_Call) Return(_a0 []float64, _a1 error) *GcpMGCPInterface_CurrentPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_CurrentPerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *GcpMGCPInterface_CurrentPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyConsumed provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) EnergyConsumed(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyConsumed") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_EnergyConsumed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyConsumed' +type GcpMGCPInterface_EnergyConsumed_Call struct { + *mock.Call +} + +// EnergyConsumed is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) EnergyConsumed(entity interface{}) *GcpMGCPInterface_EnergyConsumed_Call { + return &GcpMGCPInterface_EnergyConsumed_Call{Call: _e.mock.On("EnergyConsumed", entity)} +} + +func (_c *GcpMGCPInterface_EnergyConsumed_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_EnergyConsumed_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_EnergyConsumed_Call) Return(_a0 float64, _a1 error) *GcpMGCPInterface_EnergyConsumed_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_EnergyConsumed_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *GcpMGCPInterface_EnergyConsumed_Call { + _c.Call.Return(run) + return _c +} + +// EnergyFeedIn provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) EnergyFeedIn(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyFeedIn") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_EnergyFeedIn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyFeedIn' +type GcpMGCPInterface_EnergyFeedIn_Call struct { + *mock.Call +} + +// EnergyFeedIn is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) EnergyFeedIn(entity interface{}) *GcpMGCPInterface_EnergyFeedIn_Call { + return &GcpMGCPInterface_EnergyFeedIn_Call{Call: _e.mock.On("EnergyFeedIn", entity)} +} + +func (_c *GcpMGCPInterface_EnergyFeedIn_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_EnergyFeedIn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_EnergyFeedIn_Call) Return(_a0 float64, _a1 error) *GcpMGCPInterface_EnergyFeedIn_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_EnergyFeedIn_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *GcpMGCPInterface_EnergyFeedIn_Call { + _c.Call.Return(run) + return _c +} + +// Frequency provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) Frequency(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Frequency") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_Frequency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frequency' +type GcpMGCPInterface_Frequency_Call struct { + *mock.Call +} + +// Frequency is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) Frequency(entity interface{}) *GcpMGCPInterface_Frequency_Call { + return &GcpMGCPInterface_Frequency_Call{Call: _e.mock.On("Frequency", entity)} +} + +func (_c *GcpMGCPInterface_Frequency_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_Frequency_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_Frequency_Call) Return(_a0 float64, _a1 error) *GcpMGCPInterface_Frequency_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_Frequency_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *GcpMGCPInterface_Frequency_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// GcpMGCPInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type GcpMGCPInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) IsCompatibleEntity(entity interface{}) *GcpMGCPInterface_IsCompatibleEntity_Call { + return &GcpMGCPInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *GcpMGCPInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_IsCompatibleEntity_Call) Return(_a0 bool) *GcpMGCPInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GcpMGCPInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *GcpMGCPInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *GcpMGCPInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type GcpMGCPInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *GcpMGCPInterface_IsUseCaseSupported_Call { + return &GcpMGCPInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *GcpMGCPInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *GcpMGCPInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *GcpMGCPInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) Power(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type GcpMGCPInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) Power(entity interface{}) *GcpMGCPInterface_Power_Call { + return &GcpMGCPInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *GcpMGCPInterface_Power_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_Power_Call) Return(_a0 float64, _a1 error) *GcpMGCPInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_Power_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *GcpMGCPInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// PowerLimitationFactor provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) PowerLimitationFactor(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerLimitationFactor") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_PowerLimitationFactor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerLimitationFactor' +type GcpMGCPInterface_PowerLimitationFactor_Call struct { + *mock.Call +} + +// PowerLimitationFactor is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) PowerLimitationFactor(entity interface{}) *GcpMGCPInterface_PowerLimitationFactor_Call { + return &GcpMGCPInterface_PowerLimitationFactor_Call{Call: _e.mock.On("PowerLimitationFactor", entity)} +} + +func (_c *GcpMGCPInterface_PowerLimitationFactor_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_PowerLimitationFactor_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_PowerLimitationFactor_Call) Return(_a0 float64, _a1 error) *GcpMGCPInterface_PowerLimitationFactor_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_PowerLimitationFactor_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *GcpMGCPInterface_PowerLimitationFactor_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *GcpMGCPInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// GcpMGCPInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type GcpMGCPInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *GcpMGCPInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *GcpMGCPInterface_UpdateUseCaseAvailability_Call { + return &GcpMGCPInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *GcpMGCPInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *GcpMGCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *GcpMGCPInterface_UpdateUseCaseAvailability_Call) Return() *GcpMGCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *GcpMGCPInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *GcpMGCPInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// VoltagePerPhase provides a mock function with given fields: entity +func (_m *GcpMGCPInterface) VoltagePerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for VoltagePerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GcpMGCPInterface_VoltagePerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VoltagePerPhase' +type GcpMGCPInterface_VoltagePerPhase_Call struct { + *mock.Call +} + +// VoltagePerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *GcpMGCPInterface_Expecter) VoltagePerPhase(entity interface{}) *GcpMGCPInterface_VoltagePerPhase_Call { + return &GcpMGCPInterface_VoltagePerPhase_Call{Call: _e.mock.On("VoltagePerPhase", entity)} +} + +func (_c *GcpMGCPInterface_VoltagePerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *GcpMGCPInterface_VoltagePerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *GcpMGCPInterface_VoltagePerPhase_Call) Return(_a0 []float64, _a1 error) *GcpMGCPInterface_VoltagePerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *GcpMGCPInterface_VoltagePerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *GcpMGCPInterface_VoltagePerPhase_Call { + _c.Call.Return(run) + return _c +} + +// NewGcpMGCPInterface creates a new instance of GcpMGCPInterface. 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 NewGcpMGCPInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *GcpMGCPInterface { + mock := &GcpMGCPInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/mocks/MaMPCInterface.go b/usecases/mocks/MaMPCInterface.go new file mode 100644 index 00000000..85053bf8 --- /dev/null +++ b/usecases/mocks/MaMPCInterface.go @@ -0,0 +1,632 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package mocks + +import ( + spine_goapi "github.com/enbility/spine-go/api" + mock "github.com/stretchr/testify/mock" +) + +// MaMPCInterface is an autogenerated mock type for the MaMPCInterface type +type MaMPCInterface struct { + mock.Mock +} + +type MaMPCInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *MaMPCInterface) EXPECT() *MaMPCInterface_Expecter { + return &MaMPCInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function with given fields: +func (_m *MaMPCInterface) AddFeatures() { + _m.Called() +} + +// MaMPCInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type MaMPCInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *MaMPCInterface_Expecter) AddFeatures() *MaMPCInterface_AddFeatures_Call { + return &MaMPCInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *MaMPCInterface_AddFeatures_Call) Run(run func()) *MaMPCInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MaMPCInterface_AddFeatures_Call) Return() *MaMPCInterface_AddFeatures_Call { + _c.Call.Return() + return _c +} + +func (_c *MaMPCInterface_AddFeatures_Call) RunAndReturn(run func()) *MaMPCInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function with given fields: +func (_m *MaMPCInterface) AddUseCase() { + _m.Called() +} + +// MaMPCInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type MaMPCInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *MaMPCInterface_Expecter) AddUseCase() *MaMPCInterface_AddUseCase_Call { + return &MaMPCInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *MaMPCInterface_AddUseCase_Call) Run(run func()) *MaMPCInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MaMPCInterface_AddUseCase_Call) Return() *MaMPCInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *MaMPCInterface_AddUseCase_Call) RunAndReturn(run func()) *MaMPCInterface_AddUseCase_Call { + _c.Call.Return(run) + return _c +} + +// CurrentPerPhase provides a mock function with given fields: entity +func (_m *MaMPCInterface) CurrentPerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for CurrentPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_CurrentPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CurrentPerPhase' +type MaMPCInterface_CurrentPerPhase_Call struct { + *mock.Call +} + +// CurrentPerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) CurrentPerPhase(entity interface{}) *MaMPCInterface_CurrentPerPhase_Call { + return &MaMPCInterface_CurrentPerPhase_Call{Call: _e.mock.On("CurrentPerPhase", entity)} +} + +func (_c *MaMPCInterface_CurrentPerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_CurrentPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_CurrentPerPhase_Call) Return(_a0 []float64, _a1 error) *MaMPCInterface_CurrentPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_CurrentPerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *MaMPCInterface_CurrentPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// EnergyConsumed provides a mock function with given fields: entity +func (_m *MaMPCInterface) EnergyConsumed(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyConsumed") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_EnergyConsumed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyConsumed' +type MaMPCInterface_EnergyConsumed_Call struct { + *mock.Call +} + +// EnergyConsumed is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) EnergyConsumed(entity interface{}) *MaMPCInterface_EnergyConsumed_Call { + return &MaMPCInterface_EnergyConsumed_Call{Call: _e.mock.On("EnergyConsumed", entity)} +} + +func (_c *MaMPCInterface_EnergyConsumed_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_EnergyConsumed_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_EnergyConsumed_Call) Return(_a0 float64, _a1 error) *MaMPCInterface_EnergyConsumed_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_EnergyConsumed_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *MaMPCInterface_EnergyConsumed_Call { + _c.Call.Return(run) + return _c +} + +// EnergyProduced provides a mock function with given fields: entity +func (_m *MaMPCInterface) EnergyProduced(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for EnergyProduced") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_EnergyProduced_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnergyProduced' +type MaMPCInterface_EnergyProduced_Call struct { + *mock.Call +} + +// EnergyProduced is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) EnergyProduced(entity interface{}) *MaMPCInterface_EnergyProduced_Call { + return &MaMPCInterface_EnergyProduced_Call{Call: _e.mock.On("EnergyProduced", entity)} +} + +func (_c *MaMPCInterface_EnergyProduced_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_EnergyProduced_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_EnergyProduced_Call) Return(_a0 float64, _a1 error) *MaMPCInterface_EnergyProduced_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_EnergyProduced_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *MaMPCInterface_EnergyProduced_Call { + _c.Call.Return(run) + return _c +} + +// Frequency provides a mock function with given fields: entity +func (_m *MaMPCInterface) Frequency(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Frequency") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_Frequency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Frequency' +type MaMPCInterface_Frequency_Call struct { + *mock.Call +} + +// Frequency is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) Frequency(entity interface{}) *MaMPCInterface_Frequency_Call { + return &MaMPCInterface_Frequency_Call{Call: _e.mock.On("Frequency", entity)} +} + +func (_c *MaMPCInterface_Frequency_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_Frequency_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_Frequency_Call) Return(_a0 float64, _a1 error) *MaMPCInterface_Frequency_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_Frequency_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *MaMPCInterface_Frequency_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntity provides a mock function with given fields: entity +func (_m *MaMPCInterface) IsCompatibleEntity(entity spine_goapi.EntityRemoteInterface) bool { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntity") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MaMPCInterface_IsCompatibleEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntity' +type MaMPCInterface_IsCompatibleEntity_Call struct { + *mock.Call +} + +// IsCompatibleEntity is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) IsCompatibleEntity(entity interface{}) *MaMPCInterface_IsCompatibleEntity_Call { + return &MaMPCInterface_IsCompatibleEntity_Call{Call: _e.mock.On("IsCompatibleEntity", entity)} +} + +func (_c *MaMPCInterface_IsCompatibleEntity_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_IsCompatibleEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_IsCompatibleEntity_Call) Return(_a0 bool) *MaMPCInterface_IsCompatibleEntity_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MaMPCInterface_IsCompatibleEntity_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) bool) *MaMPCInterface_IsCompatibleEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsUseCaseSupported provides a mock function with given fields: remoteEntity +func (_m *MaMPCInterface) IsUseCaseSupported(remoteEntity spine_goapi.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(spine_goapi.EntityRemoteInterface) (bool, error)); ok { + return rf(remoteEntity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) bool); ok { + r0 = rf(remoteEntity) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(remoteEntity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_IsUseCaseSupported_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUseCaseSupported' +type MaMPCInterface_IsUseCaseSupported_Call struct { + *mock.Call +} + +// IsUseCaseSupported is a helper method to define mock.On call +// - remoteEntity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) IsUseCaseSupported(remoteEntity interface{}) *MaMPCInterface_IsUseCaseSupported_Call { + return &MaMPCInterface_IsUseCaseSupported_Call{Call: _e.mock.On("IsUseCaseSupported", remoteEntity)} +} + +func (_c *MaMPCInterface_IsUseCaseSupported_Call) Run(run func(remoteEntity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_IsUseCaseSupported_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_IsUseCaseSupported_Call) Return(_a0 bool, _a1 error) *MaMPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_IsUseCaseSupported_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (bool, error)) *MaMPCInterface_IsUseCaseSupported_Call { + _c.Call.Return(run) + return _c +} + +// Power provides a mock function with given fields: entity +func (_m *MaMPCInterface) Power(entity spine_goapi.EntityRemoteInterface) (float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Power") + } + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) (float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) float64); ok { + r0 = rf(entity) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_Power_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Power' +type MaMPCInterface_Power_Call struct { + *mock.Call +} + +// Power is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) Power(entity interface{}) *MaMPCInterface_Power_Call { + return &MaMPCInterface_Power_Call{Call: _e.mock.On("Power", entity)} +} + +func (_c *MaMPCInterface_Power_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_Power_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_Power_Call) Return(_a0 float64, _a1 error) *MaMPCInterface_Power_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_Power_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) (float64, error)) *MaMPCInterface_Power_Call { + _c.Call.Return(run) + return _c +} + +// PowerPerPhase provides a mock function with given fields: entity +func (_m *MaMPCInterface) PowerPerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for PowerPerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_PowerPerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerPerPhase' +type MaMPCInterface_PowerPerPhase_Call struct { + *mock.Call +} + +// PowerPerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) PowerPerPhase(entity interface{}) *MaMPCInterface_PowerPerPhase_Call { + return &MaMPCInterface_PowerPerPhase_Call{Call: _e.mock.On("PowerPerPhase", entity)} +} + +func (_c *MaMPCInterface_PowerPerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_PowerPerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_PowerPerPhase_Call) Return(_a0 []float64, _a1 error) *MaMPCInterface_PowerPerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_PowerPerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *MaMPCInterface_PowerPerPhase_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function with given fields: available +func (_m *MaMPCInterface) UpdateUseCaseAvailability(available bool) { + _m.Called(available) +} + +// MaMPCInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type MaMPCInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *MaMPCInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *MaMPCInterface_UpdateUseCaseAvailability_Call { + return &MaMPCInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *MaMPCInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *MaMPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(bool)) + }) + return _c +} + +func (_c *MaMPCInterface_UpdateUseCaseAvailability_Call) Return() *MaMPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *MaMPCInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(bool)) *MaMPCInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return(run) + return _c +} + +// VoltagePerPhase provides a mock function with given fields: entity +func (_m *MaMPCInterface) VoltagePerPhase(entity spine_goapi.EntityRemoteInterface) ([]float64, error) { + ret := _m.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for VoltagePerPhase") + } + + var r0 []float64 + var r1 error + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) ([]float64, error)); ok { + return rf(entity) + } + if rf, ok := ret.Get(0).(func(spine_goapi.EntityRemoteInterface) []float64); ok { + r0 = rf(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]float64) + } + } + + if rf, ok := ret.Get(1).(func(spine_goapi.EntityRemoteInterface) error); ok { + r1 = rf(entity) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MaMPCInterface_VoltagePerPhase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VoltagePerPhase' +type MaMPCInterface_VoltagePerPhase_Call struct { + *mock.Call +} + +// VoltagePerPhase is a helper method to define mock.On call +// - entity spine_goapi.EntityRemoteInterface +func (_e *MaMPCInterface_Expecter) VoltagePerPhase(entity interface{}) *MaMPCInterface_VoltagePerPhase_Call { + return &MaMPCInterface_VoltagePerPhase_Call{Call: _e.mock.On("VoltagePerPhase", entity)} +} + +func (_c *MaMPCInterface_VoltagePerPhase_Call) Run(run func(entity spine_goapi.EntityRemoteInterface)) *MaMPCInterface_VoltagePerPhase_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(spine_goapi.EntityRemoteInterface)) + }) + return _c +} + +func (_c *MaMPCInterface_VoltagePerPhase_Call) Return(_a0 []float64, _a1 error) *MaMPCInterface_VoltagePerPhase_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MaMPCInterface_VoltagePerPhase_Call) RunAndReturn(run func(spine_goapi.EntityRemoteInterface) ([]float64, error)) *MaMPCInterface_VoltagePerPhase_Call { + _c.Call.Return(run) + return _c +} + +// NewMaMPCInterface creates a new instance of MaMPCInterface. 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 NewMaMPCInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *MaMPCInterface { + mock := &MaMPCInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/usecases/usecase/testhelper_test.go b/usecases/usecase/testhelper_test.go new file mode 100644 index 00000000..292db9e3 --- /dev/null +++ b/usecases/usecase/testhelper_test.go @@ -0,0 +1,229 @@ +package usecase + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestUseCaseSuite(t *testing.T) { + suite.Run(t, new(UseCaseSuite)) +} + +type UseCaseSuite struct { + suite.Suite + + service api.ServiceInterface + + localEntity spineapi.EntityLocalInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + evseEntity spineapi.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface +} + +func (s *UseCaseSuite) Event(ski string, entity spineapi.EntityRemoteInterface, event api.EventType) { +} + +func (s *UseCaseSuite) BeforeTest(suiteName, testName string) { + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, 230.0, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + + var entities []spineapi.EntityRemoteInterface + + s.localEntity, s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.evseEntity = entities[0] + s.monitoredEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T) ( + spineapi.EntityLocalInterface, + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + localEntity := localDevice.EntityForType(model.EntityTypeTypeCEM) + + f := spine.NewFeatureLocal(1, localEntity, model.FeatureTypeTypeLoadControl, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(2, localEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(3, localEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(4, localEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeClient) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(5, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeClient) + localEntity.AddFeature(f) + 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) + f = spine.NewFeatureLocal(3, localEntity, model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + localEntity.AddFeature(f) + f = spine.NewFeatureLocal(4, localEntity, model.FeatureTypeTypeDeviceClassification, model.RoleTypeServer) + f.AddFunctionType(model.FunctionTypeDeviceClassificationManufacturerData, true, false) + f.AddFunctionType(model.FunctionTypeDeviceClassificationUserData, 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) + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + role model.RoleType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeLoadControl, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeLoadControlLimitDescriptionListData, + model.FunctionTypeLoadControlLimitConstraintsListData, + model.FunctionTypeLoadControlLimitListData, + }, + }, + {model.FeatureTypeTypeElectricalConnection, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeElectricalConnectionParameterDescriptionListData, + model.FunctionTypeElectricalConnectionPermittedValueSetListData, + }, + }, + {model.FeatureTypeTypeMeasurement, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeMeasurementDescriptionListData, + model.FunctionTypeMeasurementListData, + }, + }, + {model.FeatureTypeTypeDeviceClassification, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceClassificationManufacturerData, + model.FunctionTypeDeviceClassificationUserData, + }, + }, + {model.FeatureTypeTypeDeviceConfiguration, + model.RoleTypeServer, + []model.FunctionType{ + model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, + model.FunctionTypeDeviceConfigurationKeyValueListData, + }, + }, + } + + remoteDeviceName := "remote" + + var featureInformations []model.NodeManagementDetailedDiscoveryFeatureInformationType + for index, feature := range remoteFeatures { + supportedFcts := []model.FunctionPropertyType{} + for _, fct := range feature.supportedFcts { + supportedFct := model.FunctionPropertyType{ + Function: util.Ptr(fct), + PossibleOperations: &model.PossibleOperationsType{ + Read: &model.PossibleOperationsReadType{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(feature.role), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return localEntity, remoteDevice, entities +} diff --git a/usecases/usecase/usecase.go b/usecases/usecase/usecase.go new file mode 100644 index 00000000..8ff14e2d --- /dev/null +++ b/usecases/usecase/usecase.go @@ -0,0 +1,69 @@ +package usecase + +import ( + "slices" + + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type UseCaseBase struct { + LocalEntity spineapi.EntityLocalInterface + + UseCaseActor model.UseCaseActorType + UseCaseName model.UseCaseNameType + useCaseVersion model.SpecificationVersionType + useCaseDocumentSubVersion string + useCaseScenarios []model.UseCaseScenarioSupportType + + EventCB api.EntityEventCallback + + validEntityTypes []model.EntityTypeType +} + +var _ api.UseCaseBaseInterface = (*UseCaseBase)(nil) + +func NewUseCaseBase( + localEntity spineapi.EntityLocalInterface, + usecaseActor model.UseCaseActorType, + usecaseName model.UseCaseNameType, + useCaseVersion string, + useCaseDocumentSubVersion string, + useCaseScenarios []model.UseCaseScenarioSupportType, + eventCB api.EntityEventCallback, + validEntityTypes []model.EntityTypeType, +) *UseCaseBase { + return &UseCaseBase{ + LocalEntity: localEntity, + UseCaseActor: usecaseActor, + UseCaseName: usecaseName, + useCaseVersion: model.SpecificationVersionType(useCaseVersion), + useCaseDocumentSubVersion: useCaseDocumentSubVersion, + useCaseScenarios: useCaseScenarios, + EventCB: eventCB, + validEntityTypes: validEntityTypes, + } +} + +func (u *UseCaseBase) AddUseCase() { + u.LocalEntity.AddUseCaseSupport( + u.UseCaseActor, + u.UseCaseName, + u.useCaseVersion, + u.useCaseDocumentSubVersion, + true, + u.useCaseScenarios) +} + +func (u *UseCaseBase) UpdateUseCaseAvailability(available bool) { + u.LocalEntity.SetUseCaseAvailability(u.UseCaseActor, u.UseCaseName, available) +} + +func (u *UseCaseBase) IsCompatibleEntity(entity spineapi.EntityRemoteInterface) bool { + if entity == nil { + return false + } + + return slices.Contains(u.validEntityTypes, entity.EntityType()) +} diff --git a/usecases/usecase/usecase_test.go b/usecases/usecase/usecase_test.go new file mode 100644 index 00000000..e7b8e302 --- /dev/null +++ b/usecases/usecase/usecase_test.go @@ -0,0 +1,40 @@ +package usecase + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +func (s *UseCaseSuite) Test() { + validEntityTypes := []model.EntityTypeType{model.EntityTypeTypeEV} + uc := NewUseCaseBase( + s.localEntity, + model.UseCaseActorTypeCEM, + model.UseCaseNameTypeEVSECommissioningAndConfiguration, + "1.0.0", + "release", + []model.UseCaseScenarioSupportType{1}, + nil, + validEntityTypes, + ) + + payload := spineapi.EventPayload{} + result := uc.IsCompatibleEntity(payload.Entity) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + result = uc.IsCompatibleEntity(payload.Entity) + assert.Equal(s.T(), false, result) + + payload = spineapi.EventPayload{ + Entity: s.monitoredEntity, + } + result = uc.IsCompatibleEntity(payload.Entity) + assert.Equal(s.T(), true, result) + + uc.AddUseCase() + uc.UpdateUseCaseAvailability(false) +}