diff --git a/features/client/hvac.go b/features/client/hvac.go new file mode 100644 index 00000000..b67049e8 --- /dev/null +++ b/features/client/hvac.go @@ -0,0 +1,127 @@ +package client + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type Hvac struct { + *Feature + + *internal.HvacCommon +} + +// Get a new HVAC features helper +// +// - The feature on the local entity has to be of role client +// - The feature on the remote entity has to be of role server +func NewHvac( + localEntity spineapi.EntityLocalInterface, + remoteEntity spineapi.EntityRemoteInterface, +) (*Hvac, error) { + feature, err := NewFeature(model.FeatureTypeTypeHvac, localEntity, remoteEntity) + if err != nil { + return nil, err + } + + hvac := &Hvac{ + Feature: feature, + HvacCommon: internal.NewRemoteHvac(feature.featureRemote), + } + + return hvac, nil +} + +// request FunctionTypeHvacSystemFunctionDescriptionListData from a remote device +func (h *Hvac) RequestHvacSystemFunctionDescriptions( + selector *model.HvacSystemFunctionDescriptionListDataSelectorsType, + elements *model.HvacSystemFunctionDescriptionDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacSystemFunctionDescriptionListData, selector, elements) +} + +// request FunctionTypeHvacSystemFunctionListData from a remote device +func (h *Hvac) RequestHvacSystemFunctions( + selector *model.HvacSystemFunctionListDataSelectorsType, + elements *model.HvacSystemFunctionDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacSystemFunctionListData, selector, elements) +} + +// request FunctionTypeHvacOperationModeDescriptionListData from a remote device +func (h *Hvac) RequestHvacOperationModeDescriptions( + selector *model.HvacOperationModeDescriptionListDataSelectorsType, + elements *model.HvacOperationModeDescriptionDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacOperationModeDescriptionListData, selector, elements) +} + +// request FunctionTypeHvacSystemFunctionOperationModeRelationListData from a remote device +func (h *Hvac) RequestHvacSystemFunctionOperationModeRelations( + selector *model.HvacSystemFunctionOperationModeRelationListDataSelectorsType, + elements *model.HvacSystemFunctionOperationModeRelationDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacSystemFunctionOperationModeRelationListData, selector, elements) +} + +// request FunctionTypeHvacSystemFunctionSetPointRelationListData from a remote device +func (h *Hvac) RequestHvacSystemFunctionSetpointRelations( + selector *model.HvacSystemFunctionSetpointRelationListDataSelectorsType, + elements *model.HvacSystemFunctionSetpointRelationDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacSystemFunctionSetPointRelationListData, selector, elements) +} + +// request FunctionTypeHvacOverrunDescriptionListData from a remote device +func (h *Hvac) RequestHvacOverrunDescriptions( + selector *model.HvacOverrunDescriptionListDataSelectorsType, + elements *model.HvacOverrunDescriptionDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacOverrunDescriptionListData, selector, elements) +} + +// request FunctionTypeHvacOverrunListData from a remote device +func (h *Hvac) RequestHvacOverruns( + selector *model.HvacOverrunListDataSelectorsType, + elements *model.HvacOverrunDataElementsType, +) (*model.MsgCounterType, error) { + return h.requestData(model.FunctionTypeHvacOverrunListData, selector, elements) +} + +// write the given HVAC overrun data to the remote device, +// e.g. to start or stop an overrun +func (h *Hvac) WriteHvacOverrunListData( + data []model.HvacOverrunDataType, +) (*model.MsgCounterType, error) { + if len(data) == 0 { + return nil, api.ErrMissingData + } + + cmd := model.CmdType{ + HvacOverrunListData: &model.HvacOverrunListDataType{ + HvacOverrunData: data, + }, + } + + return h.remoteDevice.Sender().Write(h.featureLocal.Address(), h.featureRemote.Address(), cmd) +} + +// write the given HVAC system function data to the remote device, +// e.g. to change the current operation mode of a system function +func (h *Hvac) WriteHvacSystemFunctionListData( + data []model.HvacSystemFunctionDataType, +) (*model.MsgCounterType, error) { + if len(data) == 0 { + return nil, api.ErrMissingData + } + + cmd := model.CmdType{ + HvacSystemFunctionListData: &model.HvacSystemFunctionListDataType{ + HvacSystemFunctionData: data, + }, + } + + return h.remoteDevice.Sender().Write(h.featureLocal.Address(), h.featureRemote.Address(), cmd) +} diff --git a/features/client/hvac_test.go b/features/client/hvac_test.go new file mode 100644 index 00000000..d26e34c7 --- /dev/null +++ b/features/client/hvac_test.go @@ -0,0 +1,141 @@ +package client + +import ( + "testing" + + shipapi "github.com/enbility/ship-go/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" + "github.com/stretchr/testify/suite" +) + +func TestHvacSuite(t *testing.T) { + suite.Run(t, new(HvacSuite)) +} + +type HvacSuite struct { + suite.Suite + + localEntity spineapi.EntityLocalInterface + remoteEntity spineapi.EntityRemoteInterface + + hvac *Hvac + sentMessage []byte +} + +var _ shipapi.ShipConnectionDataWriterInterface = (*HvacSuite)(nil) + +func (s *HvacSuite) WriteShipMessageWithPayload(message []byte) { + s.sentMessage = message +} + +func (s *HvacSuite) BeforeTest(suiteName, testName string) { + s.localEntity, s.remoteEntity = setupFeatures( + s.T(), + s, + []featureFunctions{ + { + featureType: model.FeatureTypeTypeHvac, + functions: []model.FunctionType{ + model.FunctionTypeHvacSystemFunctionDescriptionListData, + model.FunctionTypeHvacSystemFunctionListData, + model.FunctionTypeHvacOperationModeDescriptionListData, + model.FunctionTypeHvacSystemFunctionOperationModeRelationListData, + model.FunctionTypeHvacSystemFunctionSetPointRelationListData, + model.FunctionTypeHvacOverrunDescriptionListData, + model.FunctionTypeHvacOverrunListData, + }, + }, + }, + ) + + var err error + s.hvac, err = NewHvac(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.hvac) + + s.hvac, err = NewHvac(s.localEntity, s.remoteEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), s.hvac) +} + +func (s *HvacSuite) Test_RequestHvacSystemFunctionDescriptions() { + counter, err := s.hvac.RequestHvacSystemFunctionDescriptions(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_RequestHvacSystemFunctions() { + counter, err := s.hvac.RequestHvacSystemFunctions(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_RequestHvacOperationModeDescriptions() { + counter, err := s.hvac.RequestHvacOperationModeDescriptions(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_RequestHvacSystemFunctionOperationModeRelations() { + counter, err := s.hvac.RequestHvacSystemFunctionOperationModeRelations(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_RequestHvacSystemFunctionSetpointRelations() { + counter, err := s.hvac.RequestHvacSystemFunctionSetpointRelations(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_WriteHvacSystemFunctionListData() { + counter, err := s.hvac.WriteHvacSystemFunctionListData(nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), counter) + + data := []model.HvacSystemFunctionDataType{} + counter, err = s.hvac.WriteHvacSystemFunctionListData(data) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), counter) + + data = []model.HvacSystemFunctionDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + CurrentOperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + }, + } + counter, err = s.hvac.WriteHvacSystemFunctionListData(data) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_RequestHvacOverrunDescriptions() { + counter, err := s.hvac.RequestHvacOverrunDescriptions(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_RequestHvacOverruns() { + counter, err := s.hvac.RequestHvacOverruns(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *HvacSuite) Test_WriteHvacOverrunListData() { + counter, err := s.hvac.WriteHvacOverrunListData(nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), counter) + + data := []model.HvacOverrunDataType{ + { + OverrunId: util.Ptr(model.HvacOverrunIdType(1)), + OverrunStatus: util.Ptr(model.HvacOverrunStatusTypeActive), + }, + } + counter, err = s.hvac.WriteHvacOverrunListData(data) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} diff --git a/features/client/setpoint.go b/features/client/setpoint.go new file mode 100644 index 00000000..a45a8dae --- /dev/null +++ b/features/client/setpoint.go @@ -0,0 +1,84 @@ +package client + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/internal" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type Setpoint struct { + *Feature + + *internal.SetpointCommon +} + +// Get a new Setpoint features helper +// +// - The feature on the local entity has to be of role client +// - The feature on the remote entity has to be of role server +func NewSetpoint( + localEntity spineapi.EntityLocalInterface, + remoteEntity spineapi.EntityRemoteInterface, +) (*Setpoint, error) { + feature, err := NewFeature(model.FeatureTypeTypeSetpoint, localEntity, remoteEntity) + if err != nil { + return nil, err + } + + sp := &Setpoint{ + Feature: feature, + SetpointCommon: internal.NewRemoteSetpoint(feature.featureRemote), + } + + return sp, nil +} + +// request FunctionTypeSetpointDescriptionListData from a remote device +func (s *Setpoint) RequestSetpointDescriptions( + selector *model.SetpointDescriptionListDataSelectorsType, + elements *model.SetpointDescriptionDataElementsType, +) (*model.MsgCounterType, error) { + return s.requestData(model.FunctionTypeSetpointDescriptionListData, selector, elements) +} + +// request FunctionTypeSetpointConstraintsListData from a remote device +func (s *Setpoint) RequestSetpointConstraints( + selector *model.SetpointConstraintsListDataSelectorsType, + elements *model.SetpointConstraintsDataElementsType, +) (*model.MsgCounterType, error) { + return s.requestData(model.FunctionTypeSetpointConstraintsListData, selector, elements) +} + +// request FunctionTypeSetpointListData from a remote device +func (s *Setpoint) RequestSetpoints( + selector *model.SetpointListDataSelectorsType, + elements *model.SetpointDataElementsType, +) (*model.MsgCounterType, error) { + return s.requestData(model.FunctionTypeSetpointListData, selector, elements) +} + +// write the given setpoint data to the remote device, +// returns the message counter of the sent message +func (s *Setpoint) WriteSetpointListData( + data []model.SetpointDataType, +) (*model.MsgCounterType, error) { + if len(data) == 0 { + return nil, api.ErrMissingData + } + + cmd := model.CmdType{ + SetpointListData: &model.SetpointListDataType{ + SetpointData: data, + }, + } + + return s.remoteDevice.Sender().Write(s.featureLocal.Address(), s.featureRemote.Address(), cmd) +} + +// IsSetpointListDataWritable reports whether the remote feature advertises the +// write operation for SetpointListData. +func (s *Setpoint) IsSetpointListDataWritable() bool { + operation, ok := s.featureRemote.Operations()[model.FunctionTypeSetpointListData] + return ok && operation != nil && operation.Write() +} diff --git a/features/client/setpoint_test.go b/features/client/setpoint_test.go new file mode 100644 index 00000000..1c9784a3 --- /dev/null +++ b/features/client/setpoint_test.go @@ -0,0 +1,97 @@ +package client + +import ( + "testing" + + shipapi "github.com/enbility/ship-go/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" + "github.com/stretchr/testify/suite" +) + +func TestSetpointSuite(t *testing.T) { + suite.Run(t, new(SetpointSuite)) +} + +type SetpointSuite struct { + suite.Suite + + localEntity spineapi.EntityLocalInterface + remoteEntity spineapi.EntityRemoteInterface + + setpoint *Setpoint + sentMessage []byte +} + +var _ shipapi.ShipConnectionDataWriterInterface = (*SetpointSuite)(nil) + +func (s *SetpointSuite) WriteShipMessageWithPayload(message []byte) { + s.sentMessage = message +} + +func (s *SetpointSuite) BeforeTest(suiteName, testName string) { + s.localEntity, s.remoteEntity = setupFeatures( + s.T(), + s, + []featureFunctions{ + { + featureType: model.FeatureTypeTypeSetpoint, + functions: []model.FunctionType{ + model.FunctionTypeSetpointDescriptionListData, + model.FunctionTypeSetpointConstraintsListData, + model.FunctionTypeSetpointListData, + }, + }, + }, + ) + + var err error + s.setpoint, err = NewSetpoint(s.localEntity, nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), s.setpoint) + + s.setpoint, err = NewSetpoint(s.localEntity, s.remoteEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), s.setpoint) +} + +func (s *SetpointSuite) Test_RequestSetpointDescriptions() { + counter, err := s.setpoint.RequestSetpointDescriptions(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *SetpointSuite) Test_RequestSetpointConstraints() { + counter, err := s.setpoint.RequestSetpointConstraints(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *SetpointSuite) Test_RequestSetpoints() { + counter, err := s.setpoint.RequestSetpoints(nil, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} + +func (s *SetpointSuite) Test_WriteSetpointListData() { + counter, err := s.setpoint.WriteSetpointListData(nil) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), counter) + + data := []model.SetpointDataType{} + counter, err = s.setpoint.WriteSetpointListData(data) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), counter) + + data = []model.SetpointDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + Value: model.NewScaledNumberType(21), + }, + } + counter, err = s.setpoint.WriteSetpointListData(data) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), counter) +} diff --git a/features/internal/hvac.go b/features/internal/hvac.go new file mode 100644 index 00000000..41230ff2 --- /dev/null +++ b/features/internal/hvac.go @@ -0,0 +1,222 @@ +package internal + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +type HvacCommon struct { + featureLocal spineapi.FeatureLocalInterface + featureRemote spineapi.FeatureRemoteInterface +} + +// NewLocalHvac creates a new HvacCommon helper for local entities +func NewLocalHvac(featureLocal spineapi.FeatureLocalInterface) *HvacCommon { + return &HvacCommon{ + featureLocal: featureLocal, + } +} + +// NewRemoteHvac creates a new HvacCommon helper for remote entities +func NewRemoteHvac(featureRemote spineapi.FeatureRemoteInterface) *HvacCommon { + return &HvacCommon{ + featureRemote: featureRemote, + } +} + +// GetHvacSystemFunctionDescriptionsForFilter returns the system function descriptions for a given filter +func (h *HvacCommon) GetHvacSystemFunctionDescriptionsForFilter( + filter model.HvacSystemFunctionDescriptionDataType, +) ([]model.HvacSystemFunctionDescriptionDataType, error) { + function := model.FunctionTypeHvacSystemFunctionDescriptionListData + + data, err := featureDataCopyOfType[model.HvacSystemFunctionDescriptionListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacSystemFunctionDescriptionData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacSystemFunctionDescriptionDataType](data.HvacSystemFunctionDescriptionData, filter) + + return result, nil +} + +// GetHvacSystemFunctionDescriptionForId returns the system function description for a given system function ID +func (h *HvacCommon) GetHvacSystemFunctionDescriptionForId( + id model.HvacSystemFunctionIdType, +) (*model.HvacSystemFunctionDescriptionDataType, error) { + filter := model.HvacSystemFunctionDescriptionDataType{ + SystemFunctionId: &id, + } + + result, err := h.GetHvacSystemFunctionDescriptionsForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// GetHvacSystemFunctionsForFilter returns the system function data for a given filter +func (h *HvacCommon) GetHvacSystemFunctionsForFilter( + filter model.HvacSystemFunctionDataType, +) ([]model.HvacSystemFunctionDataType, error) { + function := model.FunctionTypeHvacSystemFunctionListData + + data, err := featureDataCopyOfType[model.HvacSystemFunctionListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacSystemFunctionData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacSystemFunctionDataType](data.HvacSystemFunctionData, filter) + + return result, nil +} + +// GetHvacSystemFunctionForId returns the system function data for a given system function ID +func (h *HvacCommon) GetHvacSystemFunctionForId( + id model.HvacSystemFunctionIdType, +) (*model.HvacSystemFunctionDataType, error) { + filter := model.HvacSystemFunctionDataType{ + SystemFunctionId: &id, + } + + result, err := h.GetHvacSystemFunctionsForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// GetHvacOperationModeDescriptionsForFilter returns the operation mode descriptions for a given filter +func (h *HvacCommon) GetHvacOperationModeDescriptionsForFilter( + filter model.HvacOperationModeDescriptionDataType, +) ([]model.HvacOperationModeDescriptionDataType, error) { + function := model.FunctionTypeHvacOperationModeDescriptionListData + + data, err := featureDataCopyOfType[model.HvacOperationModeDescriptionListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacOperationModeDescriptionData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacOperationModeDescriptionDataType](data.HvacOperationModeDescriptionData, filter) + + return result, nil +} + +// GetHvacOperationModeDescriptionForId returns the operation mode description for a given operation mode ID +func (h *HvacCommon) GetHvacOperationModeDescriptionForId( + id model.HvacOperationModeIdType, +) (*model.HvacOperationModeDescriptionDataType, error) { + filter := model.HvacOperationModeDescriptionDataType{ + OperationModeId: &id, + } + + result, err := h.GetHvacOperationModeDescriptionsForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// GetHvacSystemFunctionOperationModeRelationsForFilter returns the system function +// operation mode relations for a given filter +func (h *HvacCommon) GetHvacSystemFunctionOperationModeRelationsForFilter( + filter model.HvacSystemFunctionOperationModeRelationDataType, +) ([]model.HvacSystemFunctionOperationModeRelationDataType, error) { + function := model.FunctionTypeHvacSystemFunctionOperationModeRelationListData + + data, err := featureDataCopyOfType[model.HvacSystemFunctionOperationModeRelationListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacSystemFunctionOperationModeRelationData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacSystemFunctionOperationModeRelationDataType](data.HvacSystemFunctionOperationModeRelationData, filter) + + return result, nil +} + +// GetHvacSystemFunctionSetpointRelationsForFilter returns the system function +// setpoint relations for a given filter +func (h *HvacCommon) GetHvacSystemFunctionSetpointRelationsForFilter( + filter model.HvacSystemFunctionSetpointRelationDataType, +) ([]model.HvacSystemFunctionSetpointRelationDataType, error) { + function := model.FunctionTypeHvacSystemFunctionSetPointRelationListData + + data, err := featureDataCopyOfType[model.HvacSystemFunctionSetpointRelationListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacSystemFunctionSetpointRelationData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacSystemFunctionSetpointRelationDataType](data.HvacSystemFunctionSetpointRelationData, filter) + + return result, nil +} + +// GetHvacOverrunDescriptionsForFilter returns the overrun descriptions for a given filter +func (h *HvacCommon) GetHvacOverrunDescriptionsForFilter( + filter model.HvacOverrunDescriptionDataType, +) ([]model.HvacOverrunDescriptionDataType, error) { + function := model.FunctionTypeHvacOverrunDescriptionListData + + data, err := featureDataCopyOfType[model.HvacOverrunDescriptionListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacOverrunDescriptionData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacOverrunDescriptionDataType](data.HvacOverrunDescriptionData, filter) + + return result, nil +} + +// GetHvacOverrunDataForFilter returns the overrun data for a given filter +func (h *HvacCommon) GetHvacOverrunDataForFilter( + filter model.HvacOverrunDataType, +) ([]model.HvacOverrunDataType, error) { + function := model.FunctionTypeHvacOverrunListData + + data, err := featureDataCopyOfType[model.HvacOverrunListDataType](h.featureLocal, h.featureRemote, function) + if err != nil || data == nil || data.HvacOverrunData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.HvacOverrunDataType](data.HvacOverrunData, filter) + + return result, nil +} + +// GetHvacOverrunForId returns the overrun data for a given overrun ID +func (h *HvacCommon) GetHvacOverrunForId( + id model.HvacOverrunIdType, +) (*model.HvacOverrunDataType, error) { + filter := model.HvacOverrunDataType{ + OverrunId: &id, + } + + result, err := h.GetHvacOverrunDataForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// CheckEventPayloadDataForFilter checks if the given event payload data contains +// system function data matching the given filter +func (h *HvacCommon) CheckEventPayloadDataForFilter(payloadData any, filter model.HvacSystemFunctionDataType) bool { + if payloadData == nil { + return false + } + + data, ok := payloadData.(*model.HvacSystemFunctionListDataType) + if !ok || data.HvacSystemFunctionData == nil { + return false + } + + result := searchFilterInList[model.HvacSystemFunctionDataType](data.HvacSystemFunctionData, filter) + + return len(result) > 0 +} diff --git a/features/internal/hvac_test.go b/features/internal/hvac_test.go new file mode 100644 index 00000000..76e52308 --- /dev/null +++ b/features/internal/hvac_test.go @@ -0,0 +1,405 @@ +package internal_test + +import ( + "testing" + + "github.com/enbility/eebus-go/features/internal" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestHvacSuite(t *testing.T) { + suite.Run(t, new(HvacSuite)) +} + +type HvacSuite struct { + suite.Suite + + localEntity spineapi.EntityLocalInterface + remoteEntity spineapi.EntityRemoteInterface + + localFeature spineapi.FeatureLocalInterface + remoteFeature spineapi.FeatureRemoteInterface + + localSut, + remoteSut *internal.HvacCommon +} + +func (s *HvacSuite) BeforeTest(suiteName, testName string) { + mockWriter := shipmocks.NewShipConnectionDataWriterInterface(s.T()) + mockWriter.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + + s.localEntity, s.remoteEntity = setupFeatures( + s.T(), + mockWriter, + []featureFunctions{ + { + featureType: model.FeatureTypeTypeHvac, + functions: []model.FunctionType{ + model.FunctionTypeHvacSystemFunctionDescriptionListData, + model.FunctionTypeHvacSystemFunctionListData, + model.FunctionTypeHvacOperationModeDescriptionListData, + model.FunctionTypeHvacSystemFunctionOperationModeRelationListData, + model.FunctionTypeHvacSystemFunctionSetPointRelationListData, + model.FunctionTypeHvacOverrunDescriptionListData, + model.FunctionTypeHvacOverrunListData, + }, + }, + }, + ) + + s.localFeature = s.localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeHvac, model.RoleTypeServer) + assert.NotNil(s.T(), s.localFeature) + s.localSut = internal.NewLocalHvac(s.localFeature) + assert.NotNil(s.T(), s.localSut) + + s.remoteFeature = s.remoteEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeHvac, model.RoleTypeServer) + assert.NotNil(s.T(), s.remoteFeature) + s.remoteSut = internal.NewRemoteHvac(s.remoteFeature) + assert.NotNil(s.T(), s.remoteSut) +} + +func (s *HvacSuite) Test_GetHvacSystemFunctionDescriptions() { + filter := model.HvacSystemFunctionDescriptionDataType{} + data, err := s.localSut.GetHvacSystemFunctionDescriptionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetHvacSystemFunctionDescriptionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + desc, err := s.localSut.GetHvacSystemFunctionDescriptionForId(model.HvacSystemFunctionIdType(1)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), desc) + + s.addDescriptions() + + data, err = s.localSut.GetHvacSystemFunctionDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + data, err = s.remoteSut.GetHvacSystemFunctionDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + + filter.SystemFunctionType = util.Ptr(model.HvacSystemFunctionTypeTypeHeating) + data, err = s.localSut.GetHvacSystemFunctionDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + + desc, err = s.localSut.GetHvacSystemFunctionDescriptionForId(model.HvacSystemFunctionIdType(1)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), desc) + desc, err = s.remoteSut.GetHvacSystemFunctionDescriptionForId(model.HvacSystemFunctionIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), desc) +} + +func (s *HvacSuite) Test_GetHvacSystemFunctions() { + filter := model.HvacSystemFunctionDataType{} + data, err := s.localSut.GetHvacSystemFunctionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetHvacSystemFunctionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + function, err := s.localSut.GetHvacSystemFunctionForId(model.HvacSystemFunctionIdType(1)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), function) + + s.addData() + + data, err = s.localSut.GetHvacSystemFunctionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + data, err = s.remoteSut.GetHvacSystemFunctionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + + function, err = s.localSut.GetHvacSystemFunctionForId(model.HvacSystemFunctionIdType(1)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), function) + assert.Equal(s.T(), model.HvacOperationModeIdType(2), *function.CurrentOperationModeId) + + function, err = s.remoteSut.GetHvacSystemFunctionForId(model.HvacSystemFunctionIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), function) +} + +func (s *HvacSuite) Test_GetHvacOperationModeDescriptions() { + filter := model.HvacOperationModeDescriptionDataType{} + data, err := s.localSut.GetHvacOperationModeDescriptionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetHvacOperationModeDescriptionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + desc, err := s.localSut.GetHvacOperationModeDescriptionForId(model.HvacOperationModeIdType(1)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), desc) + + s.addOperationModeDescriptions() + + data, err = s.localSut.GetHvacOperationModeDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + data, err = s.remoteSut.GetHvacOperationModeDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + + filter.OperationModeType = util.Ptr(model.HvacOperationModeTypeTypeAuto) + data, err = s.localSut.GetHvacOperationModeDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + + desc, err = s.localSut.GetHvacOperationModeDescriptionForId(model.HvacOperationModeIdType(1)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), desc) + desc, err = s.remoteSut.GetHvacOperationModeDescriptionForId(model.HvacOperationModeIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), desc) +} + +func (s *HvacSuite) Test_GetHvacSystemFunctionOperationModeRelations() { + filter := model.HvacSystemFunctionOperationModeRelationDataType{} + data, err := s.localSut.GetHvacSystemFunctionOperationModeRelationsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetHvacSystemFunctionOperationModeRelationsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + s.addOperationModeRelations() + + data, err = s.localSut.GetHvacSystemFunctionOperationModeRelationsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + data, err = s.remoteSut.GetHvacSystemFunctionOperationModeRelationsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + + filter.SystemFunctionId = util.Ptr(model.HvacSystemFunctionIdType(1)) + data, err = s.localSut.GetHvacSystemFunctionOperationModeRelationsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) +} + +func (s *HvacSuite) Test_GetHvacSystemFunctionSetpointRelations() { + filter := model.HvacSystemFunctionSetpointRelationDataType{} + data, err := s.localSut.GetHvacSystemFunctionSetpointRelationsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetHvacSystemFunctionSetpointRelationsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + s.addSetpointRelations() + + data, err = s.localSut.GetHvacSystemFunctionSetpointRelationsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + data, err = s.remoteSut.GetHvacSystemFunctionSetpointRelationsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + + filter.OperationModeId = util.Ptr(model.HvacOperationModeIdType(1)) + data, err = s.localSut.GetHvacSystemFunctionSetpointRelationsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) +} + +func (s *HvacSuite) Test_CheckEventPayloadDataForFilter() { + filter := model.HvacSystemFunctionDataType{ + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + } + + exists := s.localSut.CheckEventPayloadDataForFilter(nil, filter) + assert.False(s.T(), exists) + exists = s.remoteSut.CheckEventPayloadDataForFilter(nil, filter) + assert.False(s.T(), exists) + + temp := true + exists = s.localSut.CheckEventPayloadDataForFilter(temp, filter) + assert.False(s.T(), exists) + + data := &model.HvacSystemFunctionListDataType{} + exists = s.localSut.CheckEventPayloadDataForFilter(data, filter) + assert.False(s.T(), exists) + + data = &model.HvacSystemFunctionListDataType{ + HvacSystemFunctionData: []model.HvacSystemFunctionDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + CurrentOperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + }, + }, + } + exists = s.localSut.CheckEventPayloadDataForFilter(data, filter) + assert.True(s.T(), exists) + + filter.SystemFunctionId = util.Ptr(model.HvacSystemFunctionIdType(10)) + exists = s.localSut.CheckEventPayloadDataForFilter(data, filter) + assert.False(s.T(), exists) +} + +func (s *HvacSuite) Test_GetHvacOverruns() { + descFilter := model.HvacOverrunDescriptionDataType{} + descriptions, err := s.localSut.GetHvacOverrunDescriptionsForFilter(descFilter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), descriptions) + descriptions, err = s.remoteSut.GetHvacOverrunDescriptionsForFilter(descFilter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), descriptions) + + filter := model.HvacOverrunDataType{} + data, err := s.localSut.GetHvacOverrunDataForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + overrun, err := s.localSut.GetHvacOverrunForId(model.HvacOverrunIdType(1)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), overrun) + + s.addOverrunData() + + descFilter.OverrunType = util.Ptr(model.HvacOverrunTypeTypeOneTimeDhw) + descriptions, err = s.localSut.GetHvacOverrunDescriptionsForFilter(descFilter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(descriptions)) + descriptions, err = s.remoteSut.GetHvacOverrunDescriptionsForFilter(descFilter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(descriptions)) + + data, err = s.localSut.GetHvacOverrunDataForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + + overrun, err = s.localSut.GetHvacOverrunForId(model.HvacOverrunIdType(1)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), overrun) + assert.Equal(s.T(), model.HvacOverrunStatusTypeInactive, *overrun.OverrunStatus) + + overrun, err = s.remoteSut.GetHvacOverrunForId(model.HvacOverrunIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), overrun) +} + +// helpers + +func (s *HvacSuite) addOverrunData() { + descData := &model.HvacOverrunDescriptionListDataType{ + HvacOverrunDescriptionData: []model.HvacOverrunDescriptionDataType{ + { + OverrunId: util.Ptr(model.HvacOverrunIdType(1)), + OverrunType: util.Ptr(model.HvacOverrunTypeTypeOneTimeDhw), + AffectedSystemFunctionId: []model.HvacSystemFunctionIdType{1}, + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacOverrunDescriptionListData, descData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacOverrunDescriptionListData, descData, nil, nil) + + fData := &model.HvacOverrunListDataType{ + HvacOverrunData: []model.HvacOverrunDataType{ + { + OverrunId: util.Ptr(model.HvacOverrunIdType(1)), + OverrunStatus: util.Ptr(model.HvacOverrunStatusTypeInactive), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacOverrunListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacOverrunListData, fData, nil, nil) +} + +func (s *HvacSuite) addDescriptions() { + fData := &model.HvacSystemFunctionDescriptionListDataType{ + HvacSystemFunctionDescriptionData: []model.HvacSystemFunctionDescriptionDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + SystemFunctionType: util.Ptr(model.HvacSystemFunctionTypeTypeHeating), + }, + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(2)), + SystemFunctionType: util.Ptr(model.HvacSystemFunctionTypeTypeDhw), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacSystemFunctionDescriptionListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacSystemFunctionDescriptionListData, fData, nil, nil) +} + +func (s *HvacSuite) addData() { + fData := &model.HvacSystemFunctionListDataType{ + HvacSystemFunctionData: []model.HvacSystemFunctionDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + CurrentOperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + IsOperationModeIdChangeable: util.Ptr(true), + CurrentSetpointId: util.Ptr(model.SetpointIdType(1)), + IsSetpointIdChangeable: util.Ptr(true), + IsOverrunActive: util.Ptr(false), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacSystemFunctionListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacSystemFunctionListData, fData, nil, nil) +} + +func (s *HvacSuite) addOperationModeDescriptions() { + fData := &model.HvacOperationModeDescriptionListDataType{ + HvacOperationModeDescriptionData: []model.HvacOperationModeDescriptionDataType{ + { + OperationModeId: util.Ptr(model.HvacOperationModeIdType(1)), + OperationModeType: util.Ptr(model.HvacOperationModeTypeTypeAuto), + }, + { + OperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + OperationModeType: util.Ptr(model.HvacOperationModeTypeTypeOff), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacOperationModeDescriptionListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacOperationModeDescriptionListData, fData, nil, nil) +} + +func (s *HvacSuite) addOperationModeRelations() { + fData := &model.HvacSystemFunctionOperationModeRelationListDataType{ + HvacSystemFunctionOperationModeRelationData: []model.HvacSystemFunctionOperationModeRelationDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + OperationModeId: []model.HvacOperationModeIdType{1}, + }, + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(2)), + OperationModeId: []model.HvacOperationModeIdType{2}, + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacSystemFunctionOperationModeRelationListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacSystemFunctionOperationModeRelationListData, fData, nil, nil) +} + +func (s *HvacSuite) addSetpointRelations() { + fData := &model.HvacSystemFunctionSetpointRelationListDataType{ + HvacSystemFunctionSetpointRelationData: []model.HvacSystemFunctionSetpointRelationDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + OperationModeId: util.Ptr(model.HvacOperationModeIdType(1)), + SetpointId: []model.SetpointIdType{1}, + }, + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + OperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + SetpointId: []model.SetpointIdType{2}, + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeHvacSystemFunctionSetPointRelationListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeHvacSystemFunctionSetPointRelationListData, fData, nil, nil) +} diff --git a/features/internal/setpoint.go b/features/internal/setpoint.go new file mode 100644 index 00000000..85a81110 --- /dev/null +++ b/features/internal/setpoint.go @@ -0,0 +1,140 @@ +package internal + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" +) + +type SetpointCommon struct { + featureLocal spineapi.FeatureLocalInterface + featureRemote spineapi.FeatureRemoteInterface +} + +// NewLocalSetpoint creates a new SetpointCommon helper for local entities +func NewLocalSetpoint(featureLocal spineapi.FeatureLocalInterface) *SetpointCommon { + return &SetpointCommon{ + featureLocal: featureLocal, + } +} + +// NewRemoteSetpoint creates a new SetpointCommon helper for remote entities +func NewRemoteSetpoint(featureRemote spineapi.FeatureRemoteInterface) *SetpointCommon { + return &SetpointCommon{ + featureRemote: featureRemote, + } +} + +// GetSetpointDescriptionsForFilter returns the setpoint descriptions for a given filter +func (s *SetpointCommon) GetSetpointDescriptionsForFilter( + filter model.SetpointDescriptionDataType, +) ([]model.SetpointDescriptionDataType, error) { + function := model.FunctionTypeSetpointDescriptionListData + + data, err := featureDataCopyOfType[model.SetpointDescriptionListDataType](s.featureLocal, s.featureRemote, function) + if err != nil || data == nil || data.SetpointDescriptionData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.SetpointDescriptionDataType](data.SetpointDescriptionData, filter) + + return result, nil +} + +// GetSetpointDescriptionForId returns the setpoint description for a given setpoint ID +func (s *SetpointCommon) GetSetpointDescriptionForId( + id model.SetpointIdType, +) (*model.SetpointDescriptionDataType, error) { + filter := model.SetpointDescriptionDataType{ + SetpointId: &id, + } + + result, err := s.GetSetpointDescriptionsForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// GetSetpointDataForFilter returns the setpoint data for a given filter +func (s *SetpointCommon) GetSetpointDataForFilter( + filter model.SetpointDataType, +) ([]model.SetpointDataType, error) { + function := model.FunctionTypeSetpointListData + + data, err := featureDataCopyOfType[model.SetpointListDataType](s.featureLocal, s.featureRemote, function) + if err != nil || data == nil || data.SetpointData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.SetpointDataType](data.SetpointData, filter) + + return result, nil +} + +// GetSetpointForId returns the setpoint data for a given setpoint ID +func (s *SetpointCommon) GetSetpointForId( + id model.SetpointIdType, +) (*model.SetpointDataType, error) { + filter := model.SetpointDataType{ + SetpointId: &id, + } + + result, err := s.GetSetpointDataForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// GetSetpointConstraintsForFilter returns the setpoint constraints for a given filter +func (s *SetpointCommon) GetSetpointConstraintsForFilter( + filter model.SetpointConstraintsDataType, +) ([]model.SetpointConstraintsDataType, error) { + function := model.FunctionTypeSetpointConstraintsListData + + data, err := featureDataCopyOfType[model.SetpointConstraintsListDataType](s.featureLocal, s.featureRemote, function) + if err != nil || data == nil || data.SetpointConstraintsData == nil { + return nil, api.ErrDataNotAvailable + } + + result := searchFilterInList[model.SetpointConstraintsDataType](data.SetpointConstraintsData, filter) + + return result, nil +} + +// GetSetpointConstraintsForId returns the setpoint constraints for a given setpoint ID +func (s *SetpointCommon) GetSetpointConstraintsForId( + id model.SetpointIdType, +) (*model.SetpointConstraintsDataType, error) { + filter := model.SetpointConstraintsDataType{ + SetpointId: &id, + } + + result, err := s.GetSetpointConstraintsForFilter(filter) + if err != nil || len(result) == 0 { + return nil, api.ErrDataNotAvailable + } + + return util.Ptr(result[0]), nil +} + +// CheckEventPayloadDataForFilter checks if the given event payload data contains +// setpoint data for the given setpoint ID +func (s *SetpointCommon) CheckEventPayloadDataForFilter(payloadData any, filter model.SetpointDataType) bool { + if payloadData == nil { + return false + } + + data, ok := payloadData.(*model.SetpointListDataType) + if !ok || data.SetpointData == nil { + return false + } + + result := searchFilterInList[model.SetpointDataType](data.SetpointData, filter) + + return len(result) > 0 +} diff --git a/features/internal/setpoint_test.go b/features/internal/setpoint_test.go new file mode 100644 index 00000000..b5c12dd7 --- /dev/null +++ b/features/internal/setpoint_test.go @@ -0,0 +1,252 @@ +package internal_test + +import ( + "testing" + + "github.com/enbility/eebus-go/features/internal" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestSetpointSuite(t *testing.T) { + suite.Run(t, new(SetpointSuite)) +} + +type SetpointSuite struct { + suite.Suite + + localEntity spineapi.EntityLocalInterface + remoteEntity spineapi.EntityRemoteInterface + + localFeature spineapi.FeatureLocalInterface + remoteFeature spineapi.FeatureRemoteInterface + + localSut, + remoteSut *internal.SetpointCommon +} + +func (s *SetpointSuite) BeforeTest(suiteName, testName string) { + mockWriter := shipmocks.NewShipConnectionDataWriterInterface(s.T()) + mockWriter.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + + s.localEntity, s.remoteEntity = setupFeatures( + s.T(), + mockWriter, + []featureFunctions{ + { + featureType: model.FeatureTypeTypeSetpoint, + functions: []model.FunctionType{ + model.FunctionTypeSetpointDescriptionListData, + model.FunctionTypeSetpointConstraintsListData, + model.FunctionTypeSetpointListData, + }, + }, + }, + ) + + s.localFeature = s.localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeSetpoint, model.RoleTypeServer) + assert.NotNil(s.T(), s.localFeature) + s.localSut = internal.NewLocalSetpoint(s.localFeature) + assert.NotNil(s.T(), s.localSut) + + s.remoteFeature = s.remoteEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeSetpoint, model.RoleTypeServer) + assert.NotNil(s.T(), s.remoteFeature) + s.remoteSut = internal.NewRemoteSetpoint(s.remoteFeature) + assert.NotNil(s.T(), s.remoteSut) +} + +func (s *SetpointSuite) Test_GetSetpointDescriptions() { + filter := model.SetpointDescriptionDataType{} + data, err := s.localSut.GetSetpointDescriptionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetSetpointDescriptionsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + desc, err := s.localSut.GetSetpointDescriptionForId(model.SetpointIdType(0)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), desc) + + s.addDescriptions() + + data, err = s.localSut.GetSetpointDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + data, err = s.remoteSut.GetSetpointDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + + filter.ScopeType = util.Ptr(model.ScopeTypeTypeDhwTemperature) + data, err = s.localSut.GetSetpointDescriptionsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + + desc, err = s.localSut.GetSetpointDescriptionForId(model.SetpointIdType(0)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), desc) + desc, err = s.remoteSut.GetSetpointDescriptionForId(model.SetpointIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), desc) +} + +func (s *SetpointSuite) Test_GetSetpointData() { + filter := model.SetpointDataType{} + data, err := s.localSut.GetSetpointDataForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetSetpointDataForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + sp, err := s.localSut.GetSetpointForId(model.SetpointIdType(0)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), sp) + + s.addData() + + data, err = s.localSut.GetSetpointDataForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + data, err = s.remoteSut.GetSetpointDataForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + + sp, err = s.localSut.GetSetpointForId(model.SetpointIdType(0)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), sp) + assert.Equal(s.T(), 21.0, sp.Value.GetValue()) + + sp, err = s.remoteSut.GetSetpointForId(model.SetpointIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), sp) +} + +func (s *SetpointSuite) Test_GetSetpointConstraints() { + filter := model.SetpointConstraintsDataType{} + data, err := s.localSut.GetSetpointConstraintsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + data, err = s.remoteSut.GetSetpointConstraintsForFilter(filter) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + constraints, err := s.localSut.GetSetpointConstraintsForId(model.SetpointIdType(0)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), constraints) + + s.addConstraints() + + data, err = s.localSut.GetSetpointConstraintsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + data, err = s.remoteSut.GetSetpointConstraintsForFilter(filter) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + + constraints, err = s.localSut.GetSetpointConstraintsForId(model.SetpointIdType(0)) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), constraints) + assert.Equal(s.T(), 16.0, constraints.SetpointRangeMin.GetValue()) + assert.Equal(s.T(), 25.0, constraints.SetpointRangeMax.GetValue()) + + constraints, err = s.remoteSut.GetSetpointConstraintsForId(model.SetpointIdType(10)) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), constraints) +} + +func (s *SetpointSuite) Test_CheckEventPayloadDataForFilter() { + filter := model.SetpointDataType{ + SetpointId: util.Ptr(model.SetpointIdType(0)), + } + + exists := s.localSut.CheckEventPayloadDataForFilter(nil, filter) + assert.False(s.T(), exists) + exists = s.remoteSut.CheckEventPayloadDataForFilter(nil, filter) + assert.False(s.T(), exists) + + temp := true + exists = s.localSut.CheckEventPayloadDataForFilter(temp, filter) + assert.False(s.T(), exists) + + data := &model.SetpointListDataType{} + exists = s.localSut.CheckEventPayloadDataForFilter(data, filter) + assert.False(s.T(), exists) + + data = &model.SetpointListDataType{ + SetpointData: []model.SetpointDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(0)), + Value: model.NewScaledNumberType(21), + }, + }, + } + exists = s.localSut.CheckEventPayloadDataForFilter(data, filter) + assert.True(s.T(), exists) + + filter.SetpointId = util.Ptr(model.SetpointIdType(10)) + exists = s.localSut.CheckEventPayloadDataForFilter(data, filter) + assert.False(s.T(), exists) +} + +// helpers + +func (s *SetpointSuite) addDescriptions() { + fData := &model.SetpointDescriptionListDataType{ + SetpointDescriptionData: []model.SetpointDescriptionDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(0)), + SetpointType: util.Ptr(model.SetpointTypeTypeValueAbsolute), + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + Unit: util.Ptr(model.UnitOfMeasurementTypedegC), + }, + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + SetpointType: util.Ptr(model.SetpointTypeTypeValueAbsolute), + ScopeType: util.Ptr(model.ScopeTypeTypeRoomAirTemperature), + Unit: util.Ptr(model.UnitOfMeasurementTypedegC), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeSetpointDescriptionListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeSetpointDescriptionListData, fData, nil, nil) +} + +func (s *SetpointSuite) addData() { + fData := &model.SetpointListDataType{ + SetpointData: []model.SetpointDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(0)), + Value: model.NewScaledNumberType(21), + IsSetpointChangeable: util.Ptr(true), + }, + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + Value: model.NewScaledNumberType(45), + IsSetpointActive: util.Ptr(true), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeSetpointListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeSetpointListData, fData, nil, nil) +} + +func (s *SetpointSuite) addConstraints() { + fData := &model.SetpointConstraintsListDataType{ + SetpointConstraintsData: []model.SetpointConstraintsDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(0)), + SetpointRangeMin: model.NewScaledNumberType(16), + SetpointRangeMax: model.NewScaledNumberType(25), + SetpointStepSize: model.NewScaledNumberType(0.5), + }, + }, + } + _ = s.localFeature.UpdateData(model.FunctionTypeSetpointConstraintsListData, fData, nil, nil) + _, _ = s.remoteFeature.UpdateData(true, model.FunctionTypeSetpointConstraintsListData, fData, nil, nil) +} diff --git a/usecases/api/ca_cdt.go b/usecases/api/ca_cdt.go new file mode 100644 index 00000000..6442de33 --- /dev/null +++ b/usecases/api/ca_cdt.go @@ -0,0 +1,55 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Actor: Configuration Appliance +// UseCase: Configuration of DHW Temperature +type CaCDTInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // return the current DHW temperature setpoints + // + // parameters: + // - entity: the entity of the DHW circuit + // + // possible errors: + // - ErrDataNotAvailable if no such data is (yet) available + // - and others + Setpoints(entity spineapi.EntityRemoteInterface) ([]Setpoint, error) + + // return the constraints for the DHW temperature setpoints + // + // parameters: + // - entity: the entity of the DHW circuit + // + // possible errors: + // - ErrDataNotAvailable if no such data is (yet) available + // - and others + SetpointConstraints(entity spineapi.EntityRemoteInterface) ([]SetpointConstraints, error) + + // write the DHW temperature setpoint for a DHW operation mode + // + // parameters: + // - entity: the entity of the DHW circuit + // - mode: the DHW operation mode the setpoint is used for + // - degC: the temperature setpoint in degree Celsius + // - resultCB: called when the remote device returns a result; may be nil + // + // possible errors: + // - ErrNotSupported if the setpoint or write operation is not changeable + // - ErrDataNotAvailable if the required data is not (yet) available + // - ErrDataInvalid if the value is outside the constraints or not on a valid step + // - and others + WriteSetpoint( + entity spineapi.EntityRemoteInterface, + mode HvacOperationModeType, + degC float64, + resultCB func(result model.ResultDataType, msgCounter model.MsgCounterType), + ) (*model.MsgCounterType, error) +} diff --git a/usecases/api/types.go b/usecases/api/types.go index dc125fad..1a287a76 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -194,3 +194,49 @@ type PendingDeviceConfiguration struct { Value *model.DeviceConfigurationKeyValueValueType `json:"value,omitempty"` IsValueChangeable *bool `json:"isValueChangeable,omitempty"` } + +// operation mode of an HVAC system function +type HvacOperationModeType string + +const ( + HvacOperationModeTypeAuto HvacOperationModeType = "auto" + HvacOperationModeTypeOn HvacOperationModeType = "on" + HvacOperationModeTypeOff HvacOperationModeType = "off" + HvacOperationModeTypeEco HvacOperationModeType = "eco" +) + +// HVAC temperature setpoint, e.g. for a room or domestic hot water +type Setpoint struct { + // the setpoint identifier + Id uint + + // the setpoint temperature value + Value float64 + + // the minimum allowed temperature value + MinValue float64 + + // the maximum allowed temperature value + MaxValue float64 + + // whether the setpoint is currently active + IsActive bool + + // whether the setpoint may be changed by a client + IsChangeable bool +} + +// constraints for an HVAC temperature setpoint +type SetpointConstraints struct { + // the setpoint identifier + Id uint + + // the minimum allowed temperature value + MinValue float64 + + // the maximum allowed temperature value + MaxValue float64 + + // the step size for temperature value changes + StepSize float64 +} diff --git a/usecases/ca/cdt/events.go b/usecases/ca/cdt/events.go new file mode 100644 index 00000000..7bb89960 --- /dev/null +++ b/usecases/ca/cdt/events.go @@ -0,0 +1,117 @@ +package cdt + +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 *CDT) HandleEvent(payload spineapi.EventPayload) { + if !e.IsCompatibleEntityType(payload.Entity) { + return + } + + if internal.IsEntityAdded(payload) { + e.dhwCircuitConnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.HvacSystemFunctionDescriptionListDataType: + e.hvacSystemFunctionDescriptionDataUpdate(payload.Entity) + + case *model.SetpointDescriptionListDataType: + e.setpointDescriptionDataUpdate(payload.Entity) + + case *model.SetpointListDataType: + e.setpointDataUpdate(payload) + + case *model.SetpointConstraintsListDataType: + e.setpointConstraintsDataUpdate(payload) + } +} + +// process required steps when an DHW circuit device entity is connected +func (e *CDT) dhwCircuitConnected(entity spineapi.EntityRemoteInterface) { + if hvac, err := client.NewHvac(e.LocalEntity, entity); err == nil { + if !hvac.HasSubscription() { + if _, err := hvac.Subscribe(); err != nil { + logging.Log().Error(err) + } + } + + if _, err := hvac.RequestHvacSystemFunctionDescriptions(nil, nil); err != nil { + logging.Log().Error(err) + } + + if _, err := hvac.RequestHvacOperationModeDescriptions(nil, nil); err != nil { + logging.Log().Error(err) + } + } + + if setpoint, err := client.NewSetpoint(e.LocalEntity, entity); err == nil { + if !setpoint.HasSubscription() { + if _, err := setpoint.Subscribe(); err != nil { + logging.Log().Error(err) + } + } + + selector := &model.SetpointDescriptionListDataSelectorsType{ + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + } + if _, err := setpoint.RequestSetpointDescriptions(selector, nil); err != nil { + logging.Log().Error(err) + } + } +} + +// the HVAC system function description data of a device was updated +func (e *CDT) hvacSystemFunctionDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if hvac, err := client.NewHvac(e.LocalEntity, entity); err == nil { + // system function descriptions received, now get the setpoint relations + if _, err := hvac.RequestHvacSystemFunctionSetpointRelations(nil, nil); err != nil { + logging.Log().Error(err) + } + } +} + +// the setpoint description data of a device was updated +func (e *CDT) setpointDescriptionDataUpdate(entity spineapi.EntityRemoteInterface) { + if setpoint, err := client.NewSetpoint(e.LocalEntity, entity); err == nil { + // setpoint descriptions received, now get the constraints and data + if _, err := setpoint.RequestSetpointConstraints(nil, nil); err != nil { + logging.Log().Error(err) + } + + if _, err := setpoint.RequestSetpoints(nil, nil); err != nil { + logging.Log().Error(err) + } + } +} + +// the setpoint data of a device was updated +func (e *CDT) setpointDataUpdate(payload spineapi.EventPayload) { + if setpoint, err := client.NewSetpoint(e.LocalEntity, payload.Entity); err == nil { + filter := model.SetpointDataType{} + if setpoint.CheckEventPayloadDataForFilter(payload.Data, filter) && e.EventCB != nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateSetpoints) + } + } +} + +// the setpoint constraints data of a device was updated +func (e *CDT) setpointConstraintsDataUpdate(payload spineapi.EventPayload) { + if data, ok := payload.Data.(*model.SetpointConstraintsListDataType); ok && + len(data.SetpointConstraintsData) > 0 && e.EventCB != nil { + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateSetpointConstraints) + } +} diff --git a/usecases/ca/cdt/events_test.go b/usecases/ca/cdt/events_test.go new file mode 100644 index 00000000..6cd081bb --- /dev/null +++ b/usecases/ca/cdt/events_test.go @@ -0,0 +1,102 @@ +package cdt + +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 *CaCDTSuite) Test_Events() { + payload := spineapi.EventPayload{ + Entity: s.mockRemoteEntity, + } + s.sut.HandleEvent(payload) + + payload.Entity = s.dhwCircuitEntity + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeEntityChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.ChangeType = spineapi.ElementChangeRemove + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeAdd + s.sut.HandleEvent(payload) + + payload.EventType = spineapi.EventTypeDataChange + payload.ChangeType = spineapi.ElementChangeUpdate + payload.Data = util.Ptr(model.HvacSystemFunctionDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.SetpointDescriptionListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.SetpointListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.SetpointConstraintsListDataType{}) + s.sut.HandleEvent(payload) + + payload.Data = util.Ptr(model.NodeManagementUseCaseDataType{}) + s.sut.HandleEvent(payload) +} + +func (s *CaCDTSuite) Test_Failures() { + s.sut.dhwCircuitConnected(s.mockRemoteEntity) + + s.sut.hvacSystemFunctionDescriptionDataUpdate(s.mockRemoteEntity) + + s.sut.setpointDescriptionDataUpdate(s.mockRemoteEntity) +} + +func (s *CaCDTSuite) Test_setpointDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Entity: s.dhwCircuitEntity, + } + s.sut.setpointDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Data = util.Ptr(model.SetpointListDataType{}) + s.sut.setpointDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Data = util.Ptr(model.SetpointListDataType{ + SetpointData: []model.SetpointDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + Value: model.NewScaledNumberType(21), + }, + }, + }) + s.sut.setpointDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} + +func (s *CaCDTSuite) Test_setpointConstraintsDataUpdate() { + payload := spineapi.EventPayload{ + Ski: remoteSki, + Entity: s.dhwCircuitEntity, + } + s.sut.setpointConstraintsDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Data = util.Ptr(model.SetpointConstraintsListDataType{}) + s.sut.setpointConstraintsDataUpdate(payload) + assert.False(s.T(), s.eventCalled) + + payload.Data = util.Ptr(model.SetpointConstraintsListDataType{ + SetpointConstraintsData: []model.SetpointConstraintsDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + SetpointRangeMin: model.NewScaledNumberType(16), + }, + }, + }) + s.sut.setpointConstraintsDataUpdate(payload) + assert.True(s.T(), s.eventCalled) +} diff --git a/usecases/ca/cdt/public.go b/usecases/ca/cdt/public.go new file mode 100644 index 00000000..d8d26c03 --- /dev/null +++ b/usecases/ca/cdt/public.go @@ -0,0 +1,269 @@ +package cdt + +import ( + "math" + + "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" +) + +// Setpoints returns the current DHW temperature setpoints of the DHW circuit entity. +func (e *CDT) Setpoints(entity spineapi.EntityRemoteInterface) ([]ucapi.Setpoint, error) { + if !e.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + setpointIDs, err := e.setpointIDs(entity) + if err != nil { + return nil, err + } + + sp, err := client.NewSetpoint(e.LocalEntity, entity) + if err != nil { + return nil, err + } + + setpoints := make([]ucapi.Setpoint, 0, len(setpointIDs)) + for _, id := range setpointIDs { + data, err := sp.GetSetpointForId(id) + if err != nil || data.Value == nil { + continue + } + + setpoint := ucapi.Setpoint{ + Id: uint(id), + Value: data.Value.GetValue(), + IsActive: data.IsSetpointActive == nil || *data.IsSetpointActive, + IsChangeable: sp.IsSetpointListDataWritable() && (data.IsSetpointChangeable == nil || *data.IsSetpointChangeable), + } + if data.ValueMin != nil { + setpoint.MinValue = data.ValueMin.GetValue() + } + if data.ValueMax != nil { + setpoint.MaxValue = data.ValueMax.GetValue() + } + setpoints = append(setpoints, setpoint) + } + + if len(setpoints) == 0 { + return nil, api.ErrDataNotAvailable + } + return setpoints, nil +} + +// SetpointConstraints returns complete constraints for the DHW temperature setpoints. +func (e *CDT) SetpointConstraints(entity spineapi.EntityRemoteInterface) ([]ucapi.SetpointConstraints, error) { + if !e.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + setpointIDs, err := e.setpointIDs(entity) + if err != nil { + return nil, err + } + sp, err := client.NewSetpoint(e.LocalEntity, entity) + if err != nil { + return nil, err + } + + constraints := make([]ucapi.SetpointConstraints, 0, len(setpointIDs)) + for _, id := range setpointIDs { + data, err := sp.GetSetpointConstraintsForId(id) + if err != nil || !validConstraints(data) { + continue + } + constraints = append(constraints, ucapi.SetpointConstraints{ + Id: uint(id), + MinValue: data.SetpointRangeMin.GetValue(), + MaxValue: data.SetpointRangeMax.GetValue(), + StepSize: data.SetpointStepSize.GetValue(), + }) + } + + if len(constraints) == 0 { + return nil, api.ErrDataNotAvailable + } + return constraints, nil +} + +// WriteSetpoint writes a DHW temperature setpoint and preserves all other +// entries from the remote SetpointListData cache for devices requiring full-list writes. +func (e *CDT) WriteSetpoint( + entity spineapi.EntityRemoteInterface, + mode ucapi.HvacOperationModeType, + degC float64, + resultCB func(result model.ResultDataType, msgCounter model.MsgCounterType), +) (*model.MsgCounterType, error) { + if !e.IsCompatibleEntityType(entity) { + return nil, api.ErrNoCompatibleEntity + } + + setpointID, err := e.setpointIDForMode(entity, mode) + if err != nil { + return nil, err + } + sp, err := client.NewSetpoint(e.LocalEntity, entity) + if err != nil { + return nil, err + } + if !sp.IsSetpointListDataWritable() { + return nil, api.ErrNotSupported + } + + current, err := sp.GetSetpointForId(setpointID) + if err != nil { + return nil, err + } + if current.IsSetpointChangeable != nil && !*current.IsSetpointChangeable { + return nil, api.ErrNotSupported + } + + constraints, err := sp.GetSetpointConstraintsForId(setpointID) + if err != nil || !validConstraints(constraints) || !valueFitsConstraints(degC, constraints) { + return nil, api.ErrDataInvalid + } + + all, err := sp.GetSetpointDataForFilter(model.SetpointDataType{}) + if err != nil { + return nil, err + } + writeData := append([]model.SetpointDataType(nil), all...) + matches := 0 + for i := range writeData { + if writeData[i].SetpointId != nil && *writeData[i].SetpointId == setpointID { + writeData[i].Value = model.NewScaledNumberType(degC) + matches++ + } + } + if matches != 1 { + return nil, api.ErrDataInvalid + } + + msgCounter, err := sp.WriteSetpointListData(writeData) + if err != nil || msgCounter == nil { + if err != nil { + return nil, err + } + return nil, api.ErrDataInvalid + } + if resultCB != nil { + counter := *msgCounter + if err := sp.AddResponseCallback(counter, func(msg spineapi.ResponseMessage) { + if result, ok := msg.Data.(*model.ResultDataType); ok { + if result.ErrorNumber != nil && *result.ErrorNumber == model.ErrorNumberTypeNoError { + _, _ = sp.RequestSetpoints(nil, nil) + } + resultCB(*result, counter) + } + }); err != nil { + return msgCounter, err + } + } + return msgCounter, nil +} + +func validConstraints(data *model.SetpointConstraintsDataType) bool { + if data == nil || data.SetpointRangeMin == nil || data.SetpointRangeMax == nil || data.SetpointStepSize == nil { + return false + } + minValue := data.SetpointRangeMin.GetValue() + maxValue := data.SetpointRangeMax.GetValue() + step := data.SetpointStepSize.GetValue() + return !math.IsNaN(minValue) && !math.IsNaN(maxValue) && !math.IsNaN(step) && + !math.IsInf(minValue, 0) && !math.IsInf(maxValue, 0) && !math.IsInf(step, 0) && + minValue <= maxValue && step > 0 +} + +func valueFitsConstraints(value float64, data *model.SetpointConstraintsDataType) bool { + if !validConstraints(data) || math.IsNaN(value) || math.IsInf(value, 0) { + return false + } + minValue := data.SetpointRangeMin.GetValue() + maxValue := data.SetpointRangeMax.GetValue() + step := data.SetpointStepSize.GetValue() + if value < minValue || value > maxValue { + return false + } + steps := (value - minValue) / step + return math.Abs(steps-math.Round(steps)) <= 1e-9 +} + +func (e *CDT) setpointIDs(entity spineapi.EntityRemoteInterface) ([]model.SetpointIdType, error) { + relations, err := e.setpointRelations(entity) + if err != nil { + return nil, err + } + seen := make(map[model.SetpointIdType]struct{}) + ids := make([]model.SetpointIdType, 0) + for _, relation := range relations { + for _, id := range relation.SetpointId { + if _, exists := seen[id]; !exists { + seen[id] = struct{}{} + ids = append(ids, id) + } + } + } + if len(ids) == 0 { + return nil, api.ErrDataNotAvailable + } + return ids, nil +} + +func (e *CDT) setpointIDForMode(entity spineapi.EntityRemoteInterface, mode ucapi.HvacOperationModeType) (model.SetpointIdType, error) { + hvac, err := client.NewHvac(e.LocalEntity, entity) + if err != nil { + return 0, err + } + relations, err := e.setpointRelations(entity) + if err != nil { + return 0, err + } + + ids := make(map[model.SetpointIdType]struct{}) + for _, relation := range relations { + if relation.OperationModeId == nil { + continue + } + descriptions, err := hvac.GetHvacOperationModeDescriptionsForFilter(model.HvacOperationModeDescriptionDataType{ + OperationModeId: relation.OperationModeId, + }) + if err != nil || len(descriptions) != 1 || descriptions[0].OperationModeType == nil || + *descriptions[0].OperationModeType != model.HvacOperationModeTypeType(mode) { + continue + } + for _, id := range relation.SetpointId { + ids[id] = struct{}{} + } + } + if len(ids) != 1 { + return 0, api.ErrDataNotAvailable + } + for id := range ids { + return id, nil + } + return 0, api.ErrDataNotAvailable +} + +func (e *CDT) setpointRelations(entity spineapi.EntityRemoteInterface) ([]model.HvacSystemFunctionSetpointRelationDataType, error) { + hvac, err := client.NewHvac(e.LocalEntity, entity) + if err != nil { + return nil, err + } + descriptions, err := hvac.GetHvacSystemFunctionDescriptionsForFilter(model.HvacSystemFunctionDescriptionDataType{ + SystemFunctionType: util.Ptr(model.HvacSystemFunctionTypeTypeDhw), + }) + if err != nil || len(descriptions) != 1 || descriptions[0].SystemFunctionId == nil { + return nil, api.ErrDataNotAvailable + } + relations, err := hvac.GetHvacSystemFunctionSetpointRelationsForFilter(model.HvacSystemFunctionSetpointRelationDataType{ + SystemFunctionId: descriptions[0].SystemFunctionId, + }) + if err != nil || len(relations) == 0 { + return nil, api.ErrDataNotAvailable + } + return relations, nil +} diff --git a/usecases/ca/cdt/public_test.go b/usecases/ca/cdt/public_test.go new file mode 100644 index 00000000..6fb67eea --- /dev/null +++ b/usecases/ca/cdt/public_test.go @@ -0,0 +1,187 @@ +package cdt + +import ( + "encoding/json" + + "github.com/enbility/eebus-go/api" + 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 *CaCDTSuite) Test_Setpoints() { + data, err := s.sut.Setpoints(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.Setpoints(s.dhwCircuitEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + s.addHvacData() + + data, err = s.sut.Setpoints(s.dhwCircuitEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + s.addSetpointData() + + data, err = s.sut.Setpoints(s.dhwCircuitEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 2, len(data)) + assert.Equal(s.T(), uint(1), data[0].Id) + assert.Equal(s.T(), 21.0, data[0].Value) + assert.True(s.T(), data[0].IsChangeable) + assert.True(s.T(), data[1].IsActive) +} + +func (s *CaCDTSuite) Test_SetpointConstraints() { + data, err := s.sut.SetpointConstraints(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + data, err = s.sut.SetpointConstraints(s.dhwCircuitEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + s.addHvacData() + s.addSetpointData() + + data, err = s.sut.SetpointConstraints(s.dhwCircuitEntity) + assert.Nil(s.T(), err) + assert.Equal(s.T(), 1, len(data)) + assert.Equal(s.T(), uint(1), data[0].Id) + assert.Equal(s.T(), 16.0, data[0].MinValue) + assert.Equal(s.T(), 25.0, data[0].MaxValue) + assert.Equal(s.T(), 0.5, data[0].StepSize) +} + +func (s *CaCDTSuite) Test_WriteSetpoint() { + _, err := s.sut.WriteSetpoint(s.mockRemoteEntity, ucapi.HvacOperationModeTypeEco, 19, nil) + assert.NotNil(s.T(), err) + + _, err = s.sut.WriteSetpoint(s.dhwCircuitEntity, ucapi.HvacOperationModeTypeEco, 19, nil) + assert.NotNil(s.T(), err) + + s.addHvacData() + s.addSetpointData() + + msgCounter, err := s.sut.WriteSetpoint(s.dhwCircuitEntity, ucapi.HvacOperationModeTypeEco, 19, nil) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), msgCounter) + + // Preserve unrelated entries for remotes that require full-list writes. + var datagram model.Datagram + assert.NoError(s.T(), json.Unmarshal(s.sentBytes, &datagram)) + assert.Len(s.T(), datagram.Datagram.Payload.Cmd, 1) + written := datagram.Datagram.Payload.Cmd[0].SetpointListData.SetpointData + assert.Len(s.T(), written, 2) + assert.Equal(s.T(), 19.0, written[0].Value.GetValue()) + assert.Equal(s.T(), 23.0, written[1].Value.GetValue()) + + // the off mode has no setpoint in the test data + _, err = s.sut.WriteSetpoint(s.dhwCircuitEntity, ucapi.HvacOperationModeTypeOff, 19, nil) + assert.NotNil(s.T(), err) + + // a setpoint marked not changeable cannot be written + _, err = s.sut.WriteSetpoint(s.dhwCircuitEntity, ucapi.HvacOperationModeTypeOn, 22, nil) + assert.NotNil(s.T(), err) + + // values outside the advertised range or step are rejected locally + _, err = s.sut.WriteSetpoint(s.dhwCircuitEntity, ucapi.HvacOperationModeTypeEco, 26, nil) + assert.ErrorIs(s.T(), err, api.ErrDataInvalid) + _, err = s.sut.WriteSetpoint(s.dhwCircuitEntity, ucapi.HvacOperationModeTypeEco, 19.25, nil) + assert.ErrorIs(s.T(), err, api.ErrDataInvalid) +} + +// helpers + +func (s *CaCDTSuite) addHvacData() { + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.dhwCircuitEntity, model.FeatureTypeTypeHvac, model.RoleTypeServer) + + descData := &model.HvacSystemFunctionDescriptionListDataType{ + HvacSystemFunctionDescriptionData: []model.HvacSystemFunctionDescriptionDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + SystemFunctionType: util.Ptr(model.HvacSystemFunctionTypeTypeDhw), + }, + }, + } + _, fErr := rFeature.UpdateData(true, model.FunctionTypeHvacSystemFunctionDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + modeData := &model.HvacOperationModeDescriptionListDataType{ + HvacOperationModeDescriptionData: []model.HvacOperationModeDescriptionDataType{ + { + OperationModeId: util.Ptr(model.HvacOperationModeIdType(1)), + OperationModeType: util.Ptr(model.HvacOperationModeTypeTypeEco), + }, + { + OperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + OperationModeType: util.Ptr(model.HvacOperationModeTypeTypeOn), + }, + { + OperationModeId: util.Ptr(model.HvacOperationModeIdType(3)), + OperationModeType: util.Ptr(model.HvacOperationModeTypeTypeOff), + }, + }, + } + _, fErr = rFeature.UpdateData(true, model.FunctionTypeHvacOperationModeDescriptionListData, modeData, nil, nil) + assert.Nil(s.T(), fErr) + + relationData := &model.HvacSystemFunctionSetpointRelationListDataType{ + HvacSystemFunctionSetpointRelationData: []model.HvacSystemFunctionSetpointRelationDataType{ + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + OperationModeId: util.Ptr(model.HvacOperationModeIdType(1)), + SetpointId: []model.SetpointIdType{1}, + }, + { + SystemFunctionId: util.Ptr(model.HvacSystemFunctionIdType(1)), + OperationModeId: util.Ptr(model.HvacOperationModeIdType(2)), + SetpointId: []model.SetpointIdType{2}, + }, + }, + } + _, fErr = rFeature.UpdateData(true, model.FunctionTypeHvacSystemFunctionSetPointRelationListData, relationData, nil, nil) + assert.Nil(s.T(), fErr) +} + +func (s *CaCDTSuite) addSetpointData() { + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.dhwCircuitEntity, model.FeatureTypeTypeSetpoint, model.RoleTypeServer) + + spData := &model.SetpointListDataType{ + SetpointData: []model.SetpointDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + Value: model.NewScaledNumberType(21), + ValueMin: model.NewScaledNumberType(16), + ValueMax: model.NewScaledNumberType(25), + // An omitted flag means changeable; only explicit false blocks writes. + IsSetpointChangeable: nil, + }, + { + SetpointId: util.Ptr(model.SetpointIdType(2)), + Value: model.NewScaledNumberType(23), + IsSetpointActive: util.Ptr(true), + IsSetpointChangeable: util.Ptr(false), + }, + }, + } + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSetpointListData, spData, nil, nil) + assert.Nil(s.T(), fErr) + + constraintsData := &model.SetpointConstraintsListDataType{ + SetpointConstraintsData: []model.SetpointConstraintsDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + SetpointRangeMin: model.NewScaledNumberType(16), + SetpointRangeMax: model.NewScaledNumberType(25), + SetpointStepSize: model.NewScaledNumberType(0.5), + }, + }, + } + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSetpointConstraintsListData, constraintsData, nil, nil) + assert.Nil(s.T(), fErr) +} diff --git a/usecases/ca/cdt/testhelper_test.go b/usecases/ca/cdt/testhelper_test.go new file mode 100644 index 00000000..4ab16875 --- /dev/null +++ b/usecases/ca/cdt/testhelper_test.go @@ -0,0 +1,189 @@ +package cdt + +import ( + "fmt" + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + shipmocks "github.com/enbility/ship-go/mocks" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestCaCDTSuite(t *testing.T) { + suite.Run(t, new(CaCDTSuite)) +} + +type CaCDTSuite struct { + suite.Suite + + sut *CDT + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + dhwCircuitEntity spineapi.EntityRemoteInterface + + eventCalled bool + sentBytes []byte +} + +func (s *CaCDTSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *CaCDTSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + s.sentBytes = nil + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, time.Second*4, nil, nil) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCDT(localEntity, s.Event) + _ = s.sut.AddFeatures() + s.sut.AddUseCase() + + s.remoteDevice, s.dhwCircuitEntity = setupDevices(s.service, s.T(), func(message []byte) { + s.sentBytes = append(s.sentBytes[:0], message...) + }) +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, t *testing.T, onWrite func([]byte)) ( + spineapi.DeviceRemoteInterface, + spineapi.EntityRemoteInterface) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Run(func(message []byte) { + // Keep an owned copy: spine-go may reuse the sender buffer. + onWrite(message) + }).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + {model.FeatureTypeTypeSetpoint, + []model.FunctionType{ + model.FunctionTypeSetpointDescriptionListData, + model.FunctionTypeSetpointConstraintsListData, + model.FunctionTypeSetpointListData, + }, + }, + {model.FeatureTypeTypeHvac, + []model.FunctionType{ + model.FunctionTypeHvacSystemFunctionDescriptionListData, + model.FunctionTypeHvacOperationModeDescriptionListData, + model.FunctionTypeHvacSystemFunctionSetPointRelationListData, + }, + }, + } + 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{}, + }, + } + if feature.featureType == model.FeatureTypeTypeSetpoint && fct == model.FunctionTypeSetpointListData { + supportedFct.PossibleOperations.Write = &model.PossibleOperationsWriteType{} + } + 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.EntityTypeTypeDHWCircuit), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData, nil) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + for _, entity := range entities { + entity.UpdateDeviceAddress(*remoteDevice.Address()) + } + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities[0] +} diff --git a/usecases/ca/cdt/types.go b/usecases/ca/cdt/types.go new file mode 100644 index 00000000..be3b815f --- /dev/null +++ b/usecases/ca/cdt/types.go @@ -0,0 +1,24 @@ +package cdt + +import "github.com/enbility/eebus-go/api" + +const ( + // Update of the list of remote entities supporting the Use Case + // + // Use `RemoteEntities` to get the current data + UseCaseSupportUpdate api.EventType = "ca-cdt-UseCaseSupportUpdate" + + // DHW temperature setpoint data updated + // + // Use `Setpoints` to get the current data + // + // Use Case CDT, Scenario 1 + DataUpdateSetpoints api.EventType = "ca-cdt-DataUpdateSetpoints" + + // DHW temperature setpoint constraints updated + // + // Use `SetpointConstraints` to get the current data + // + // Use Case CDT, Scenario 1 + DataUpdateSetpointConstraints api.EventType = "ca-cdt-DataUpdateSetpointConstraints" +) diff --git a/usecases/ca/cdt/usecase.go b/usecases/ca/cdt/usecase.go new file mode 100644 index 00000000..a74882f6 --- /dev/null +++ b/usecases/ca/cdt/usecase.go @@ -0,0 +1,74 @@ +package cdt + +import ( + "errors" + + "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" +) + +type CDT struct { + *usecase.UseCaseBase +} + +var _ ucapi.CaCDTInterface = (*CDT)(nil) + +// Add support for the Configuration of DHW Temperature (CDT) +// use case as a Configuration Appliance actor +// +// Parameters: +// - localEntity: The local entity which should support the use case +// - eventCB: The callback to be called when an event is triggered (optional, can be nil) +func NewCDT(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback) *CDT { + validActorTypes := []model.UseCaseActorType{model.UseCaseActorTypeDHWCircuit} + validEntityTypes := []model.EntityTypeType{model.EntityTypeTypeDHWCircuit} + useCaseScenarios := []api.UseCaseScenario{ + { + Scenario: model.UseCaseScenarioSupportType(1), + Mandatory: true, + ServerFeatures: []model.FeatureTypeType{ + model.FeatureTypeTypeSetpoint, + model.FeatureTypeTypeHvac, + }, + }, + } + + usecase := usecase.NewUseCaseBase( + localEntity, + model.UseCaseActorTypeConfigurationAppliance, + model.UseCaseNameTypeConfigurationOfDhwTemperature, + "1.0.0", + "release", + useCaseScenarios, + eventCB, + UseCaseSupportUpdate, + validActorTypes, + validEntityTypes, + false) + + uc := &CDT{ + UseCaseBase: usecase, + } + + _ = localEntity.Device().Events().Subscribe(uc) + + return uc +} + +func (e *CDT) AddFeatures() error { + // client features + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeSetpoint, + model.FeatureTypeTypeHvac, + } + for _, feature := range clientFeatures { + if f := e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient); f == nil { + return errors.New("could not add feature: " + string(feature)) + } + } + + return nil +} diff --git a/usecases/ca/cdt/usecase_test.go b/usecases/ca/cdt/usecase_test.go new file mode 100644 index 00000000..353f6476 --- /dev/null +++ b/usecases/ca/cdt/usecase_test.go @@ -0,0 +1,5 @@ +package cdt + +func (s *CaCDTSuite) Test_UpdateUseCaseAvailability() { + s.sut.UpdateUseCaseAvailability(true) +} diff --git a/usecases/mocks/CaCDTInterface.go b/usecases/mocks/CaCDTInterface.go new file mode 100644 index 00000000..8b8e2cff --- /dev/null +++ b/usecases/mocks/CaCDTInterface.go @@ -0,0 +1,601 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + api0 "github.com/enbility/eebus-go/api" + api1 "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + mock "github.com/stretchr/testify/mock" +) + +// NewCaCDTInterface creates a new instance of CaCDTInterface. 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 NewCaCDTInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CaCDTInterface { + mock := &CaCDTInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// CaCDTInterface is an autogenerated mock type for the CaCDTInterface type +type CaCDTInterface struct { + mock.Mock +} + +type CaCDTInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *CaCDTInterface) EXPECT() *CaCDTInterface_Expecter { + return &CaCDTInterface_Expecter{mock: &_m.Mock} +} + +// AddFeatures provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) AddFeatures() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for AddFeatures") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// CaCDTInterface_AddFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFeatures' +type CaCDTInterface_AddFeatures_Call struct { + *mock.Call +} + +// AddFeatures is a helper method to define mock.On call +func (_e *CaCDTInterface_Expecter) AddFeatures() *CaCDTInterface_AddFeatures_Call { + return &CaCDTInterface_AddFeatures_Call{Call: _e.mock.On("AddFeatures")} +} + +func (_c *CaCDTInterface_AddFeatures_Call) Run(run func()) *CaCDTInterface_AddFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CaCDTInterface_AddFeatures_Call) Return(err error) *CaCDTInterface_AddFeatures_Call { + _c.Call.Return(err) + return _c +} + +func (_c *CaCDTInterface_AddFeatures_Call) RunAndReturn(run func() error) *CaCDTInterface_AddFeatures_Call { + _c.Call.Return(run) + return _c +} + +// AddUseCase provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) AddUseCase() { + _mock.Called() + return +} + +// CaCDTInterface_AddUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUseCase' +type CaCDTInterface_AddUseCase_Call struct { + *mock.Call +} + +// AddUseCase is a helper method to define mock.On call +func (_e *CaCDTInterface_Expecter) AddUseCase() *CaCDTInterface_AddUseCase_Call { + return &CaCDTInterface_AddUseCase_Call{Call: _e.mock.On("AddUseCase")} +} + +func (_c *CaCDTInterface_AddUseCase_Call) Run(run func()) *CaCDTInterface_AddUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CaCDTInterface_AddUseCase_Call) Return() *CaCDTInterface_AddUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CaCDTInterface_AddUseCase_Call) RunAndReturn(run func()) *CaCDTInterface_AddUseCase_Call { + _c.Run(run) + return _c +} + +// AvailableScenariosForEntity provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) AvailableScenariosForEntity(entity api.EntityRemoteInterface) []uint { + ret := _mock.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for AvailableScenariosForEntity") + } + + var r0 []uint + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface) []uint); ok { + r0 = returnFunc(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]uint) + } + } + return r0 +} + +// CaCDTInterface_AvailableScenariosForEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AvailableScenariosForEntity' +type CaCDTInterface_AvailableScenariosForEntity_Call struct { + *mock.Call +} + +// AvailableScenariosForEntity is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *CaCDTInterface_Expecter) AvailableScenariosForEntity(entity interface{}) *CaCDTInterface_AvailableScenariosForEntity_Call { + return &CaCDTInterface_AvailableScenariosForEntity_Call{Call: _e.mock.On("AvailableScenariosForEntity", entity)} +} + +func (_c *CaCDTInterface_AvailableScenariosForEntity_Call) Run(run func(entity api.EntityRemoteInterface)) *CaCDTInterface_AvailableScenariosForEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 api.EntityRemoteInterface + if args[0] != nil { + arg0 = args[0].(api.EntityRemoteInterface) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *CaCDTInterface_AvailableScenariosForEntity_Call) Return(uints []uint) *CaCDTInterface_AvailableScenariosForEntity_Call { + _c.Call.Return(uints) + return _c +} + +func (_c *CaCDTInterface_AvailableScenariosForEntity_Call) RunAndReturn(run func(entity api.EntityRemoteInterface) []uint) *CaCDTInterface_AvailableScenariosForEntity_Call { + _c.Call.Return(run) + return _c +} + +// IsCompatibleEntityType provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) IsCompatibleEntityType(entity api.EntityRemoteInterface) bool { + ret := _mock.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for IsCompatibleEntityType") + } + + var r0 bool + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface) bool); ok { + r0 = returnFunc(entity) + } else { + r0 = ret.Get(0).(bool) + } + return r0 +} + +// CaCDTInterface_IsCompatibleEntityType_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsCompatibleEntityType' +type CaCDTInterface_IsCompatibleEntityType_Call struct { + *mock.Call +} + +// IsCompatibleEntityType is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *CaCDTInterface_Expecter) IsCompatibleEntityType(entity interface{}) *CaCDTInterface_IsCompatibleEntityType_Call { + return &CaCDTInterface_IsCompatibleEntityType_Call{Call: _e.mock.On("IsCompatibleEntityType", entity)} +} + +func (_c *CaCDTInterface_IsCompatibleEntityType_Call) Run(run func(entity api.EntityRemoteInterface)) *CaCDTInterface_IsCompatibleEntityType_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 api.EntityRemoteInterface + if args[0] != nil { + arg0 = args[0].(api.EntityRemoteInterface) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *CaCDTInterface_IsCompatibleEntityType_Call) Return(b bool) *CaCDTInterface_IsCompatibleEntityType_Call { + _c.Call.Return(b) + return _c +} + +func (_c *CaCDTInterface_IsCompatibleEntityType_Call) RunAndReturn(run func(entity api.EntityRemoteInterface) bool) *CaCDTInterface_IsCompatibleEntityType_Call { + _c.Call.Return(run) + return _c +} + +// IsScenarioAvailableAtEntity provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) IsScenarioAvailableAtEntity(entity api.EntityRemoteInterface, scenario uint) bool { + ret := _mock.Called(entity, scenario) + + if len(ret) == 0 { + panic("no return value specified for IsScenarioAvailableAtEntity") + } + + var r0 bool + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface, uint) bool); ok { + r0 = returnFunc(entity, scenario) + } else { + r0 = ret.Get(0).(bool) + } + return r0 +} + +// CaCDTInterface_IsScenarioAvailableAtEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsScenarioAvailableAtEntity' +type CaCDTInterface_IsScenarioAvailableAtEntity_Call struct { + *mock.Call +} + +// IsScenarioAvailableAtEntity is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - scenario uint +func (_e *CaCDTInterface_Expecter) IsScenarioAvailableAtEntity(entity interface{}, scenario interface{}) *CaCDTInterface_IsScenarioAvailableAtEntity_Call { + return &CaCDTInterface_IsScenarioAvailableAtEntity_Call{Call: _e.mock.On("IsScenarioAvailableAtEntity", entity, scenario)} +} + +func (_c *CaCDTInterface_IsScenarioAvailableAtEntity_Call) Run(run func(entity api.EntityRemoteInterface, scenario uint)) *CaCDTInterface_IsScenarioAvailableAtEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 api.EntityRemoteInterface + if args[0] != nil { + arg0 = args[0].(api.EntityRemoteInterface) + } + var arg1 uint + if args[1] != nil { + arg1 = args[1].(uint) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *CaCDTInterface_IsScenarioAvailableAtEntity_Call) Return(b bool) *CaCDTInterface_IsScenarioAvailableAtEntity_Call { + _c.Call.Return(b) + return _c +} + +func (_c *CaCDTInterface_IsScenarioAvailableAtEntity_Call) RunAndReturn(run func(entity api.EntityRemoteInterface, scenario uint) bool) *CaCDTInterface_IsScenarioAvailableAtEntity_Call { + _c.Call.Return(run) + return _c +} + +// RemoteEntitiesScenarios provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) RemoteEntitiesScenarios() []api0.RemoteEntityScenarios { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for RemoteEntitiesScenarios") + } + + var r0 []api0.RemoteEntityScenarios + if returnFunc, ok := ret.Get(0).(func() []api0.RemoteEntityScenarios); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api0.RemoteEntityScenarios) + } + } + return r0 +} + +// CaCDTInterface_RemoteEntitiesScenarios_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteEntitiesScenarios' +type CaCDTInterface_RemoteEntitiesScenarios_Call struct { + *mock.Call +} + +// RemoteEntitiesScenarios is a helper method to define mock.On call +func (_e *CaCDTInterface_Expecter) RemoteEntitiesScenarios() *CaCDTInterface_RemoteEntitiesScenarios_Call { + return &CaCDTInterface_RemoteEntitiesScenarios_Call{Call: _e.mock.On("RemoteEntitiesScenarios")} +} + +func (_c *CaCDTInterface_RemoteEntitiesScenarios_Call) Run(run func()) *CaCDTInterface_RemoteEntitiesScenarios_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CaCDTInterface_RemoteEntitiesScenarios_Call) Return(remoteEntityScenarioss []api0.RemoteEntityScenarios) *CaCDTInterface_RemoteEntitiesScenarios_Call { + _c.Call.Return(remoteEntityScenarioss) + return _c +} + +func (_c *CaCDTInterface_RemoteEntitiesScenarios_Call) RunAndReturn(run func() []api0.RemoteEntityScenarios) *CaCDTInterface_RemoteEntitiesScenarios_Call { + _c.Call.Return(run) + return _c +} + +// RemoveUseCase provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) RemoveUseCase() { + _mock.Called() + return +} + +// CaCDTInterface_RemoveUseCase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveUseCase' +type CaCDTInterface_RemoveUseCase_Call struct { + *mock.Call +} + +// RemoveUseCase is a helper method to define mock.On call +func (_e *CaCDTInterface_Expecter) RemoveUseCase() *CaCDTInterface_RemoveUseCase_Call { + return &CaCDTInterface_RemoveUseCase_Call{Call: _e.mock.On("RemoveUseCase")} +} + +func (_c *CaCDTInterface_RemoveUseCase_Call) Run(run func()) *CaCDTInterface_RemoveUseCase_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *CaCDTInterface_RemoveUseCase_Call) Return() *CaCDTInterface_RemoveUseCase_Call { + _c.Call.Return() + return _c +} + +func (_c *CaCDTInterface_RemoveUseCase_Call) RunAndReturn(run func()) *CaCDTInterface_RemoveUseCase_Call { + _c.Run(run) + return _c +} + +// SetpointConstraints provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) SetpointConstraints(entity api.EntityRemoteInterface) ([]api1.SetpointConstraints, error) { + ret := _mock.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for SetpointConstraints") + } + + var r0 []api1.SetpointConstraints + var r1 error + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]api1.SetpointConstraints, error)); ok { + return returnFunc(entity) + } + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface) []api1.SetpointConstraints); ok { + r0 = returnFunc(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api1.SetpointConstraints) + } + } + if returnFunc, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = returnFunc(entity) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// CaCDTInterface_SetpointConstraints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetpointConstraints' +type CaCDTInterface_SetpointConstraints_Call struct { + *mock.Call +} + +// SetpointConstraints is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *CaCDTInterface_Expecter) SetpointConstraints(entity interface{}) *CaCDTInterface_SetpointConstraints_Call { + return &CaCDTInterface_SetpointConstraints_Call{Call: _e.mock.On("SetpointConstraints", entity)} +} + +func (_c *CaCDTInterface_SetpointConstraints_Call) Run(run func(entity api.EntityRemoteInterface)) *CaCDTInterface_SetpointConstraints_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 api.EntityRemoteInterface + if args[0] != nil { + arg0 = args[0].(api.EntityRemoteInterface) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *CaCDTInterface_SetpointConstraints_Call) Return(setpointConstraintss []api1.SetpointConstraints, err error) *CaCDTInterface_SetpointConstraints_Call { + _c.Call.Return(setpointConstraintss, err) + return _c +} + +func (_c *CaCDTInterface_SetpointConstraints_Call) RunAndReturn(run func(entity api.EntityRemoteInterface) ([]api1.SetpointConstraints, error)) *CaCDTInterface_SetpointConstraints_Call { + _c.Call.Return(run) + return _c +} + +// Setpoints provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) Setpoints(entity api.EntityRemoteInterface) ([]api1.Setpoint, error) { + ret := _mock.Called(entity) + + if len(ret) == 0 { + panic("no return value specified for Setpoints") + } + + var r0 []api1.Setpoint + var r1 error + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface) ([]api1.Setpoint, error)); ok { + return returnFunc(entity) + } + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface) []api1.Setpoint); ok { + r0 = returnFunc(entity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api1.Setpoint) + } + } + if returnFunc, ok := ret.Get(1).(func(api.EntityRemoteInterface) error); ok { + r1 = returnFunc(entity) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// CaCDTInterface_Setpoints_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Setpoints' +type CaCDTInterface_Setpoints_Call struct { + *mock.Call +} + +// Setpoints is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +func (_e *CaCDTInterface_Expecter) Setpoints(entity interface{}) *CaCDTInterface_Setpoints_Call { + return &CaCDTInterface_Setpoints_Call{Call: _e.mock.On("Setpoints", entity)} +} + +func (_c *CaCDTInterface_Setpoints_Call) Run(run func(entity api.EntityRemoteInterface)) *CaCDTInterface_Setpoints_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 api.EntityRemoteInterface + if args[0] != nil { + arg0 = args[0].(api.EntityRemoteInterface) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *CaCDTInterface_Setpoints_Call) Return(setpoints []api1.Setpoint, err error) *CaCDTInterface_Setpoints_Call { + _c.Call.Return(setpoints, err) + return _c +} + +func (_c *CaCDTInterface_Setpoints_Call) RunAndReturn(run func(entity api.EntityRemoteInterface) ([]api1.Setpoint, error)) *CaCDTInterface_Setpoints_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUseCaseAvailability provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) UpdateUseCaseAvailability(available bool) { + _mock.Called(available) + return +} + +// CaCDTInterface_UpdateUseCaseAvailability_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUseCaseAvailability' +type CaCDTInterface_UpdateUseCaseAvailability_Call struct { + *mock.Call +} + +// UpdateUseCaseAvailability is a helper method to define mock.On call +// - available bool +func (_e *CaCDTInterface_Expecter) UpdateUseCaseAvailability(available interface{}) *CaCDTInterface_UpdateUseCaseAvailability_Call { + return &CaCDTInterface_UpdateUseCaseAvailability_Call{Call: _e.mock.On("UpdateUseCaseAvailability", available)} +} + +func (_c *CaCDTInterface_UpdateUseCaseAvailability_Call) Run(run func(available bool)) *CaCDTInterface_UpdateUseCaseAvailability_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 bool + if args[0] != nil { + arg0 = args[0].(bool) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *CaCDTInterface_UpdateUseCaseAvailability_Call) Return() *CaCDTInterface_UpdateUseCaseAvailability_Call { + _c.Call.Return() + return _c +} + +func (_c *CaCDTInterface_UpdateUseCaseAvailability_Call) RunAndReturn(run func(available bool)) *CaCDTInterface_UpdateUseCaseAvailability_Call { + _c.Run(run) + return _c +} + +// WriteSetpoint provides a mock function for the type CaCDTInterface +func (_mock *CaCDTInterface) WriteSetpoint(entity api.EntityRemoteInterface, mode api1.HvacOperationModeType, degC float64, resultCB func(result model.ResultDataType, msgCounter model.MsgCounterType)) (*model.MsgCounterType, error) { + ret := _mock.Called(entity, mode, degC, resultCB) + + if len(ret) == 0 { + panic("no return value specified for WriteSetpoint") + } + + var r0 *model.MsgCounterType + var r1 error + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface, api1.HvacOperationModeType, float64, func(result model.ResultDataType, msgCounter model.MsgCounterType)) (*model.MsgCounterType, error)); ok { + return returnFunc(entity, mode, degC, resultCB) + } + if returnFunc, ok := ret.Get(0).(func(api.EntityRemoteInterface, api1.HvacOperationModeType, float64, func(result model.ResultDataType, msgCounter model.MsgCounterType)) *model.MsgCounterType); ok { + r0 = returnFunc(entity, mode, degC, resultCB) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.MsgCounterType) + } + } + if returnFunc, ok := ret.Get(1).(func(api.EntityRemoteInterface, api1.HvacOperationModeType, float64, func(result model.ResultDataType, msgCounter model.MsgCounterType)) error); ok { + r1 = returnFunc(entity, mode, degC, resultCB) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// CaCDTInterface_WriteSetpoint_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteSetpoint' +type CaCDTInterface_WriteSetpoint_Call struct { + *mock.Call +} + +// WriteSetpoint is a helper method to define mock.On call +// - entity api.EntityRemoteInterface +// - mode api1.HvacOperationModeType +// - degC float64 +// - resultCB func(result model.ResultDataType, msgCounter model.MsgCounterType) +func (_e *CaCDTInterface_Expecter) WriteSetpoint(entity interface{}, mode interface{}, degC interface{}, resultCB interface{}) *CaCDTInterface_WriteSetpoint_Call { + return &CaCDTInterface_WriteSetpoint_Call{Call: _e.mock.On("WriteSetpoint", entity, mode, degC, resultCB)} +} + +func (_c *CaCDTInterface_WriteSetpoint_Call) Run(run func(entity api.EntityRemoteInterface, mode api1.HvacOperationModeType, degC float64, resultCB func(result model.ResultDataType, msgCounter model.MsgCounterType))) *CaCDTInterface_WriteSetpoint_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 api.EntityRemoteInterface + if args[0] != nil { + arg0 = args[0].(api.EntityRemoteInterface) + } + var arg1 api1.HvacOperationModeType + if args[1] != nil { + arg1 = args[1].(api1.HvacOperationModeType) + } + var arg2 float64 + if args[2] != nil { + arg2 = args[2].(float64) + } + var arg3 func(result model.ResultDataType, msgCounter model.MsgCounterType) + if args[3] != nil { + arg3 = args[3].(func(result model.ResultDataType, msgCounter model.MsgCounterType)) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *CaCDTInterface_WriteSetpoint_Call) Return(msgCounterType *model.MsgCounterType, err error) *CaCDTInterface_WriteSetpoint_Call { + _c.Call.Return(msgCounterType, err) + return _c +} + +func (_c *CaCDTInterface_WriteSetpoint_Call) RunAndReturn(run func(entity api.EntityRemoteInterface, mode api1.HvacOperationModeType, degC float64, resultCB func(result model.ResultDataType, msgCounter model.MsgCounterType)) (*model.MsgCounterType, error)) *CaCDTInterface_WriteSetpoint_Call { + _c.Call.Return(run) + return _c +}