From e4322d52cba7ac98d471e67fb1d760c4db760b94 Mon Sep 17 00:00:00 2001 From: David Sapir Date: Tue, 29 Oct 2024 16:35:42 +0200 Subject: [PATCH] Feat: Add CDT Support as CA (Configuration Appliance) Client Actor This commit introduces CDT support as a CA (Configuration Appliance) client actor. It provides full support for the CDT use case, specifically Scenario 1. This includes the ability to retrieve setpoints, their constraints, and map them to their corresponding operation modes via HvacSetpointRelations. For the use case to fully function, support for the CDSF (Configuration of DHW System Function) use case is necessary. Specifically, we need to request HvacOperationModeDescriptionListDataType, which is used in conjunction with HvacSystemFunctionSetpointRelationListDataType to establish the mapping between operation modes and their setpoints, and to enable the ability to write setpoints. Note: Writing setpoints was tested and confirmed to work with Vaillant's HeatPump by requesting the HvacOperationModeDescriptionListDataType message and performing the mapping without the CDSF use case. Resources used (specifications): - EEBus UC Technical Specification Configuration of DHW Temperature - EEBus SPINE Technical Specification Resource Specification --- features/client/hvac.go | 50 ++++++++ features/client/setpoint.go | 83 ++++++++++++ features/internal/hvac.go | 51 ++++++++ features/internal/setpoint.go | 127 +++++++++++++++++++ go.mod | 4 +- go.sum | 10 +- usecases/api/ca_cdt.go | 47 +++++++ usecases/api/types.go | 47 +++++++ usecases/ca/cdt/events.go | 143 +++++++++++++++++++++ usecases/ca/cdt/public.go | 165 ++++++++++++++++++++++++ usecases/ca/cdt/public_test.go | 183 +++++++++++++++++++++++++++ usecases/ca/cdt/testhelper_test.go | 197 +++++++++++++++++++++++++++++ usecases/ca/cdt/types.go | 24 ++++ usecases/ca/cdt/usecase.go | 76 +++++++++++ usecases/ca/cdt/usecase_test.go | 5 + 15 files changed, 1206 insertions(+), 6 deletions(-) create mode 100644 features/client/hvac.go create mode 100644 features/client/setpoint.go create mode 100644 features/internal/hvac.go create mode 100644 features/internal/setpoint.go create mode 100644 usecases/api/ca_cdt.go create mode 100644 usecases/ca/cdt/events.go create mode 100644 usecases/ca/cdt/public.go create mode 100644 usecases/ca/cdt/public_test.go create mode 100644 usecases/ca/cdt/testhelper_test.go create mode 100644 usecases/ca/cdt/types.go create mode 100644 usecases/ca/cdt/usecase.go create mode 100644 usecases/ca/cdt/usecase_test.go diff --git a/features/client/hvac.go b/features/client/hvac.go new file mode 100644 index 00000000..77a33246 --- /dev/null +++ b/features/client/hvac.go @@ -0,0 +1,50 @@ +package client + +import ( + "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 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 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) +} diff --git a/features/client/setpoint.go b/features/client/setpoint.go new file mode 100644 index 00000000..075811df --- /dev/null +++ b/features/client/setpoint.go @@ -0,0 +1,83 @@ +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) +} + +// WriteSetPointListData writes the given setpoint data +// +// Parameters: +// - data: the setpoint data to write +// +// Returns: +// - the message counter of the sent message +// - an error if the data could not be written +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) +} diff --git a/features/internal/hvac.go b/features/internal/hvac.go new file mode 100644 index 00000000..b3c7a7e7 --- /dev/null +++ b/features/internal/hvac.go @@ -0,0 +1,51 @@ +package internal + +import ( + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +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, + } +} + +// GetHvacOperationModeDescriptions returns the operation mode descriptions +func (h *HvacCommon) GetHvacOperationModeDescriptions() ([]model.HvacOperationModeDescriptionDataType, error) { + function := model.FunctionTypeHvacOperationModeDescriptionListData + operationModeDescriptions := make([]model.HvacOperationModeDescriptionDataType, 0) + + data, err := featureDataCopyOfType[model.HvacOperationModeDescriptionListDataType](h.featureLocal, h.featureRemote, function) + if err == nil || data != nil { + operationModeDescriptions = append(operationModeDescriptions, data.HvacOperationModeDescriptionData...) + } + + return operationModeDescriptions, nil +} + +// GetHvacSystemFunctionSetpointRelations returns the operation mode relations (used to map operation modes to setpoints) +func (h *HvacCommon) GetHvacSystemFunctionSetpointRelations() ([]model.HvacSystemFunctionSetpointRelationDataType, error) { + function := model.FunctionTypeHvacSystemFunctionSetPointRelationListData + relations := make([]model.HvacSystemFunctionSetpointRelationDataType, 0) + + data, err := featureDataCopyOfType[model.HvacSystemFunctionSetpointRelationListDataType](h.featureLocal, h.featureRemote, function) + if err == nil || data != nil { + relations = append(relations, data.HvacSystemFunctionSetpointRelationData...) + } + + return relations, nil +} diff --git a/features/internal/setpoint.go b/features/internal/setpoint.go new file mode 100644 index 00000000..51dcfa5a --- /dev/null +++ b/features/internal/setpoint.go @@ -0,0 +1,127 @@ +package internal + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/ship-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +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, + } +} + +// GetSetpointDescriptions returns the setpoint descriptions +func (s *SetPointCommon) GetSetpointDescriptions() ([]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 + } + + return data.SetpointDescriptionData, 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 +} + +// GetSetpoints returns the setpoints +func (s *SetPointCommon) GetSetpoints() []model.SetpointDataType { + function := model.FunctionTypeSetpointListData + + data, err := featureDataCopyOfType[model.SetpointListDataType](s.featureLocal, s.featureRemote, function) + if err != nil || data == nil || data.SetpointData == nil { + return []model.SetpointDataType{} + } + + return data.SetpointData +} + +// 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 +} + +// GetSetpointConstraints returns the setpoints constraints. +func (s *SetPointCommon) GetSetpointConstraints() []model.SetpointConstraintsDataType { + function := model.FunctionTypeSetpointConstraintsListData + + data, err := featureDataCopyOfType[model.SetpointConstraintsListDataType](s.featureLocal, s.featureRemote, function) + if err != nil || data == nil || data.SetpointConstraintsData == nil { + return []model.SetpointConstraintsDataType{} + } + + return data.SetpointConstraintsData +} + +// 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 +} + +// 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 +} diff --git a/go.mod b/go.mod index b960fd80..df235b47 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/enbility/eebus-go go 1.22.0 require ( - github.com/enbility/ship-go v0.0.0-20241006160314-3a4325a1a6d6 - github.com/enbility/spine-go v0.0.0-20241007182100-30ee8bc405a7 + github.com/enbility/ship-go v0.6.1-0.20241023165311-5963bf4d9424 + github.com/enbility/spine-go v0.7.1-0.20241023170915-0b14938a9a37 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index 0888b791..adc197e6 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a h1:foChWb8lhzqa6lWDRs6COYMdp649YlUirFP8GqoT0JQ= github.com/enbility/go-avahi v0.0.0-20240909195612-d5de6b280d7a/go.mod h1:H64mhYcAQUGUUnVqMdZQf93kPecH4M79xwH95Lddt3U= -github.com/enbility/ship-go v0.0.0-20241006160314-3a4325a1a6d6 h1:bjrcJ4wxEsG5rXHlXnedRzqAV9JYglj82S14Nf1oLvs= -github.com/enbility/ship-go v0.0.0-20241006160314-3a4325a1a6d6/go.mod h1:JJp8EQcJhUhTpZ2LSEU4rpdaM3E2n08tswWFWtmm/wU= -github.com/enbility/spine-go v0.0.0-20241007182100-30ee8bc405a7 h1:n6tv+YUMncSR9qxUs6k7d/YsKD9ujHHp5pUspIvM6sc= -github.com/enbility/spine-go v0.0.0-20241007182100-30ee8bc405a7/go.mod h1:ZoI9TaJO/So/677uknrli8sc6iryD7wC5iWhVIre+MI= +github.com/enbility/ship-go v0.6.1-0.20241023165311-5963bf4d9424 h1:yzf1pWKZn+vhxtWE1ZNyspAX8GiX/r4uBXZU+SdidNY= +github.com/enbility/ship-go v0.6.1-0.20241023165311-5963bf4d9424/go.mod h1:JJp8EQcJhUhTpZ2LSEU4rpdaM3E2n08tswWFWtmm/wU= +github.com/enbility/spine-go v0.7.0 h1:UZeghFgnM3VFU0ghc57Htt6gnxwP9jLppfU2GUMJGgY= +github.com/enbility/spine-go v0.7.0/go.mod h1:IF1sBTr7p3wXqlejeBJcJ8BYFlzzRaZcJsGw8XjgEgc= +github.com/enbility/spine-go v0.7.1-0.20241023170915-0b14938a9a37 h1:oZFPU4fHYBbSMVCwP3c9GHov8dFXqqQ2McvEyalsBY8= +github.com/enbility/spine-go v0.7.1-0.20241023170915-0b14938a9a37/go.mod h1:ZoI9TaJO/So/677uknrli8sc6iryD7wC5iWhVIre+MI= github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6 h1:XOYvxKtT1oxT37w/5oEiRLuPbm9FuJPt3fiYhX0h8Po= github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:BszP9qFV14mPXgyIREbgIdQtWxbAj3OKqvK02HihMoM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= diff --git a/usecases/api/ca_cdt.go b/usecases/api/ca_cdt.go new file mode 100644 index 00000000..ee53d3a5 --- /dev/null +++ b/usecases/api/ca_cdt.go @@ -0,0 +1,47 @@ +package api + +import ( + "github.com/enbility/eebus-go/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +type CaCDTInterface interface { + api.UseCaseInterface + + // Scenario 1 + + // Return the current setpoints data + // + // parameters: + // - entity: the entity to get the setpoints data from + // + // return values: + // - setpoints: A map of the setpoints for supported modes + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + Setpoints(entity spineapi.EntityRemoteInterface) ([]Setpoint, error) + + // Return the constraints for the setpoints + // + // parameters: + // - entity: the entity to get the setpoints constraints from + // + // return values: + // - setpointConstraints: A map of the constraints for supported modes + // + // possible errors: + // - ErrDataNotAvailable if no such limit is (yet) available + // - and others + SetpointConstraints(entity spineapi.EntityRemoteInterface) ([]SetpointConstraints, error) + + // Write a setpoint + // + // parameters: + // - entity: the entity to write the setpoint to + // - mode: the mode to write the setpoint for + // - degC: the temperature setpoint value to write + WriteSetpoint(entity spineapi.EntityRemoteInterface, mode model.HvacOperationModeTypeType, degC float64) error +} diff --git a/usecases/api/types.go b/usecases/api/types.go index a5661b76..c2516e0e 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -150,3 +150,50 @@ type DurationSlotValue struct { Duration time.Duration // Duration of this slot Value float64 // Energy Cost or Power Limit } + +// Contains details about the setpoint. +// Note: At least one of the following elements must be set (combinations may be used): Value, Min, Max +type Setpoint struct { + // The ID of the setpoint. + Id uint + + // The setpoint value, 0 if not set. + Value float64 + + // Lower limit of the value range, 0 if not set. + MinValue float64 + + // Upper limit of the value range, 0 if not set. + MaxValue float64 + + // Maximum absolute variation allowed around the setpoint, 0 if not set. + AbsoluteValueTolerance float64 + + // Maximum percentage variation allowed around the setpoint, 0 if not set. + PercentageValueTolerance float64 + + // Indicates if the setpoint is changeable by a client. + IsChangeable bool + + // Indicates if the setpoint is currently active. + IsActive bool + + // The period during which the setpoint is active, 0 if not set. + TimePeriod model.TimePeriodType +} + +// Contains details about the setpoint constraints. +// Constraints that shall be held when trying to change a setpoint. +type SetpointConstraints struct { + // the ID of the setpoint + Id uint + + // Minimum value, the setpoint can be set to, 0 if not set. + MinValue float64 + + // Maximum value, the setpoint can be set to, 0 if not set. + MaxValue float64 + + // Minimum step size between two different values, 0 if not set. + StepSize float64 +} diff --git a/usecases/ca/cdt/events.go b/usecases/ca/cdt/events.go new file mode 100644 index 00000000..8a74cc5c --- /dev/null +++ b/usecases/ca/cdt/events.go @@ -0,0 +1,143 @@ +package cdt + +import ( + "github.com/enbility/eebus-go/features/client" + "github.com/enbility/eebus-go/usecases/internal" + "github.com/enbility/ship-go/logging" + "github.com/enbility/ship-go/util" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// HandleEvent handles events for the CDT use case. +func (e *CDT) HandleEvent(payload spineapi.EventPayload) { + if !e.IsCompatibleEntityType(payload.Entity) { + return + } + + if internal.IsEntityConnected(payload) { + e.dhwCircuitconnected(payload.Entity) + return + } + + if payload.EventType != spineapi.EventTypeDataChange || + payload.ChangeType != spineapi.ElementChangeUpdate { + return + } + + switch payload.Data.(type) { + case *model.SetpointDescriptionListDataType: + e.setpointDescriptionsUpdate(payload) + + case *model.SetpointConstraintsListDataType: + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateSetpointConstraints) + + case *model.SetpointListDataType: + e.EventCB(payload.Ski, payload.Device, payload.Entity, DataUpdateSetpoints) + + case *model.HvacOperationModeDescriptionListDataType, + *model.HvacSystemFunctionSetpointRelationListDataType: + e.mapSetpointsToOperationModes(payload) + } +} + +// mapSetpointsToOperationModes maps setpoints to operation modes. +// +// Details: +// Per the EEBus specification, HVAC system function setpoint relations map operation modes to setpoints. +// Operation modes are detailed in the HVAC operation mode descriptions message which is received by another usecase (CDSF). +// Each operation mode, except Auto (which can have up to 4 setpoints), should have at most one associated setpoint. +func (e *CDT) mapSetpointsToOperationModes(payload spineapi.EventPayload) { + hvac, err := client.NewHvac(e.LocalEntity, payload.Entity) + if err != nil { + logging.Log().Debug(err) + } + + relations, _ := hvac.GetHvacSystemFunctionSetpointRelations() + opModeDescriptions, _ := hvac.GetHvacOperationModeDescriptions() + if len(opModeDescriptions) == 0 || len(relations) == 0 { + return + } + + operationModeForId := make(map[model.HvacOperationModeIdType]model.HvacOperationModeTypeType) + for _, description := range opModeDescriptions { + operationModeForId[*description.OperationModeId] = *description.OperationModeType + } + + setpointsForOperationMode := make(map[model.HvacOperationModeTypeType][]model.SetpointIdType) + for _, relation := range relations { + mode := operationModeForId[*relation.OperationModeId] + setpointsForOperationMode[mode] = append(setpointsForOperationMode[mode], relation.SetpointId...) + } + + clear(e.setpointsForOperationMode) + for mode, setpointIDs := range setpointsForOperationMode { + if len(setpointIDs) != 1 && mode != model.HvacOperationModeTypeTypeAuto { + logging.Log().Errorf("Operation mode '%s' has %d setpoints", mode, len(setpointIDs)) + continue + } + + e.setpointsForOperationMode[mode] = setpointIDs[0] + } +} + +// setpointDescriptionsUpdate processes the necessary steps when setpoint descriptions are updated. +func (e *CDT) setpointDescriptionsUpdate(payload spineapi.EventPayload) { + setPoint, err := client.NewSetpoint(e.LocalEntity, payload.Entity) + if err != nil { + logging.Log().Debug(err) + return + } + + // The setpointConstraintsListData and setpointListData reads should + // be partial, using setpointId from setpointDescriptionListData. + setpointDescriptions := payload.Data.(*model.SetpointDescriptionListDataType).SetpointDescriptionData + for _, setpointDescription := range setpointDescriptions { + constraintsSelector := &model.SetpointConstraintsListDataSelectorsType{ + SetpointId: setpointDescription.SetpointId, + } + if _, err := setPoint.RequestSetPointConstraints(constraintsSelector, nil); err != nil { + logging.Log().Debug(err) + } + + setpointSelector := &model.SetpointListDataSelectorsType{ + SetpointId: setpointDescription.SetpointId, + } + if _, err := setPoint.RequestSetPoints(setpointSelector, nil); err != nil { + logging.Log().Debug(err) + } + } + + // Request setpoint relations to map operation modes to setpoints. + if hvac, err := client.NewHvac(e.LocalEntity, payload.Entity); err == nil { + if _, err := hvac.RequestHvacSystemFunctionSetPointRelations(nil, nil); err != nil { + logging.Log().Debug(err) + } + } +} + +// dhwCircuitconnected processes required steps when a DHW Circuit 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().Debug(err) + } + } + } + + if setPoint, err := client.NewSetpoint(e.LocalEntity, entity); err == nil { + if !setPoint.HasSubscription() { + if _, err := setPoint.Subscribe(); err != nil { + logging.Log().Debug(err) + } + } + + selector := &model.SetpointDescriptionListDataSelectorsType{ + ScopeType: util.Ptr(model.ScopeTypeTypeDhwTemperature), + } + if _, err := setPoint.RequestSetPointDescriptions(selector, nil); err != nil { + logging.Log().Debug(err) + } + } +} diff --git a/usecases/ca/cdt/public.go b/usecases/ca/cdt/public.go new file mode 100644 index 00000000..35a1a526 --- /dev/null +++ b/usecases/ca/cdt/public.go @@ -0,0 +1,165 @@ +package cdt + +import ( + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/client" + usecasesapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Setpoints returns the setpoints. +// +// Possible errors: +// - ErrDataNotAvailable: If the mapping of operation modes to setpoints or the setpoints themselves are not available. +// - Other errors: Any other errors encountered during the process. +func (e *CDT) Setpoints(entity spineapi.EntityRemoteInterface) ([]usecasesapi.Setpoint, error) { + setpoints := make([]usecasesapi.Setpoint, 0) + + sp, err := client.NewSetpoint(e.LocalEntity, entity) + if err != nil { + return nil, err + } + + for _, setpoint := range sp.GetSetpoints() { + var value float64 = 0 + var minValue float64 = 0 + var maxValue float64 = 0 + var timePeriod model.TimePeriodType = model.TimePeriodType{} + + if setpoint.SetpointId == nil { + logging.Log().Error("Setpoint ID is nil") + continue + } + + if setpoint.Value != nil { + value = setpoint.Value.GetValue() + } + + if setpoint.ValueMax != nil { + maxValue = setpoint.ValueMax.GetValue() + } + + if setpoint.ValueMin != nil { + minValue = setpoint.ValueMin.GetValue() + } + + if setpoint.TimePeriod != nil { + timePeriod = *setpoint.TimePeriod + } + + isActive := (setpoint.IsSetpointActive == nil || *setpoint.IsSetpointActive) + isChangeable := (setpoint.IsSetpointChangeable == nil || *setpoint.IsSetpointChangeable) + + setpoints = append(setpoints, + usecasesapi.Setpoint{ + Id: uint(*setpoint.SetpointId), + Value: value, + MinValue: minValue, + MaxValue: maxValue, + IsActive: isActive, + IsChangeable: isChangeable, + TimePeriod: timePeriod, + }, + ) + } + + if len(setpoints) == 0 { + return nil, api.ErrDataNotAvailable + } + + return setpoints, nil +} + +// SetpointConstraints returns the setpoint constraints. +// +// Possible errors: +// - ErrDataNotAvailable: If the mapping of operation modes to setpoints or the setpoint constraints are not available. +// - Other errors: Any other errors encountered during the process. +func (e *CDT) SetpointConstraints(entity spineapi.EntityRemoteInterface) ([]usecasesapi.SetpointConstraints, error) { + setpointConstraints := make([]usecasesapi.SetpointConstraints, 0) + + sp, err := client.NewSetpoint(e.LocalEntity, entity) + if err != nil { + return nil, err + } + + for _, constraints := range sp.GetSetpointConstraints() { + var minValue float64 = 0 + var maxValue float64 = 0 + var setSize float64 = 0 + + if constraints.SetpointId == nil { + logging.Log().Error("Setpoint ID is nil") + continue + } + + if constraints.SetpointRangeMin != nil { + minValue = constraints.SetpointRangeMin.GetValue() + } + + if constraints.SetpointRangeMax != nil { + maxValue = constraints.SetpointRangeMax.GetValue() + } + + if constraints.SetpointStepSize != nil { + setSize = constraints.SetpointStepSize.GetValue() + } + + setpointConstraints = append(setpointConstraints, + usecasesapi.SetpointConstraints{ + Id: uint(*constraints.SetpointId), + MinValue: minValue, + MaxValue: maxValue, + StepSize: setSize, + }, + ) + } + + if len(setpointConstraints) == 0 { + return nil, api.ErrDataNotAvailable + } + + return setpointConstraints, nil +} + +// WriteSetpoint sets the temperature setpoint for a specific operation mode. +// +// Possible errors: +// - ErrDataNotAvailable: If the mapping of operation modes to setpoints is not available. +// - ErrNotSupported: If the setpoint is not changeable. +// - Other errors: Any other errors encountered during the process. +func (e *CDT) WriteSetpoint( + entity spineapi.EntityRemoteInterface, + mode model.HvacOperationModeTypeType, + temperature float64, +) error { + if mode == model.HvacOperationModeTypeTypeAuto { + // HW Circuit selects a setpoint in this mode based on time table or other conditions. + return nil + } + + setpointId, found := e.setpointsForOperationMode[mode] + if !found { + return api.ErrDataNotAvailable + } + + setPoint, err := client.NewSetpoint(e.LocalEntity, entity) + if err != nil { + return err + } + + setpointToWrite := []model.SetpointDataType{ + { + SetpointId: &setpointId, + Value: model.NewScaledNumberType(temperature), + }, + } + + if _, err = setPoint.WriteSetPointListData(setpointToWrite); err != nil { + return err + } + + return nil +} diff --git a/usecases/ca/cdt/public_test.go b/usecases/ca/cdt/public_test.go new file mode 100644 index 00000000..efc0dd1e --- /dev/null +++ b/usecases/ca/cdt/public_test.go @@ -0,0 +1,183 @@ +package cdt + +import ( + "github.com/enbility/ship-go/util" + "github.com/enbility/spine-go/model" + "github.com/stretchr/testify/assert" +) + +// Test_Setpoints verifies the retrieval of setpoints from a remote entity. +func (s *CaCDTSuite) Test_Setpoints() { + // Test case: No setpoints available for mock remote entity + data, err := s.sut.Setpoints(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + // Test case: No setpoints available for CDT entity + data, err = s.sut.Setpoints(s.cdtEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + // Prepare setpoint data + setpointsData := &model.SetpointListDataType{} + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.cdtEntity, model.FeatureTypeTypeSetpoint, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSetpointListData, setpointsData, nil, nil) + assert.Nil(s.T(), fErr) + + // Test case: No setpoints available after updating data + data, err = s.sut.Setpoints(s.cdtEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + // Add setpoint data + setpointsData.SetpointData = []model.SetpointDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + Value: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(34)), + Scale: util.Ptr(model.ScaleType(0)), + }), + ValueMin: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(20)), + Scale: util.Ptr(model.ScaleType(0)), + }), + ValueMax: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(70)), + Scale: util.Ptr(model.ScaleType(0)), + }), + ValueToleranceAbsolute: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(0)), + Scale: util.Ptr(model.ScaleType(0)), + }), + ValueTolerancePercentage: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(0)), + Scale: util.Ptr(model.ScaleType(0)), + }), + IsSetpointChangeable: util.Ptr(true), + IsSetpointActive: util.Ptr(true), + TimePeriod: util.Ptr(model.TimePeriodType{}), + }, + } + + // Update data with new setpoint data + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSetpointListData, setpointsData, nil, nil) + assert.Nil(s.T(), fErr) + + // Test case: Setpoints available after updating data + data, err = s.sut.Setpoints(s.cdtEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Len(s.T(), data, 1) + assert.Equal(s.T(), uint(1), data[0].Id) + assert.Equal(s.T(), 34.0, data[0].Value) + assert.Equal(s.T(), 20.0, data[0].MinValue) + assert.Equal(s.T(), 70.0, data[0].MaxValue) + assert.True(s.T(), data[0].IsActive) + assert.True(s.T(), data[0].IsChangeable) + assert.Equal(s.T(), model.TimePeriodType{}, data[0].TimePeriod) +} + +// Test_SetpointConstraints verifies the retrieval of setpoint constraints from a remote entity. +func (s *CaCDTSuite) Test_SetpointConstraints() { + // Test case: No setpoint constraints available for mock remote entity + data, err := s.sut.SetpointConstraints(s.mockRemoteEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + // Test case: No setpoint constraints available for CDT entity + data, err = s.sut.SetpointConstraints(s.cdtEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + // Prepare setpoint constraints data + constraintsData := &model.SetpointConstraintsListDataType{} + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.cdtEntity, model.FeatureTypeTypeSetpoint, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSetpointConstraintsListData, constraintsData, nil, nil) + assert.Nil(s.T(), fErr) + + // Test case: No setpoint constraints available after updating data + data, err = s.sut.SetpointConstraints(s.cdtEntity) + assert.NotNil(s.T(), err) + assert.Nil(s.T(), data) + + // Add setpoint constraints data + constraintsData.SetpointConstraintsData = []model.SetpointConstraintsDataType{ + { + SetpointId: util.Ptr(model.SetpointIdType(1)), + SetpointRangeMin: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(20)), + Scale: util.Ptr(model.ScaleType(0)), + }), + SetpointRangeMax: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(70)), + Scale: util.Ptr(model.ScaleType(0)), + }), + SetpointStepSize: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(1)), + Scale: util.Ptr(model.ScaleType(0)), + }), + }, + } + + // Update data with new setpoint constraints data + _, fErr = rFeature.UpdateData(true, model.FunctionTypeSetpointConstraintsListData, constraintsData, nil, nil) + assert.Nil(s.T(), fErr) + + // Test case: Setpoint constraints available after updating data + data, err = s.sut.SetpointConstraints(s.cdtEntity) + assert.Nil(s.T(), err) + assert.NotNil(s.T(), data) + assert.Len(s.T(), data, 1) + assert.Equal(s.T(), uint(1), data[0].Id) + assert.Equal(s.T(), 20.0, data[0].MinValue) + assert.Equal(s.T(), 70.0, data[0].MaxValue) + assert.Equal(s.T(), 1.0, data[0].StepSize) +} + +// Test_WriteSetpoint verifies the functionality of writing a setpoint to a remote entity. +func (s *CaCDTSuite) Test_WriteSetpoint() { + // Test case: No setpoints available for mock remote entity + err := s.sut.WriteSetpoint(s.mockRemoteEntity, model.HvacOperationModeTypeTypeOn, 35.0) + assert.NotNil(s.T(), err) + + // Create a setpoint + setpoint := model.SetpointDataType{ + SetpointId: util.Ptr(model.SetpointIdType(1)), + Value: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(34)), + Scale: util.Ptr(model.ScaleType(0)), + }), + ValueMin: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(20)), + Scale: util.Ptr(model.ScaleType(0)), + }), + ValueMax: util.Ptr(model.ScaledNumberType{ + Number: util.Ptr(model.NumberType(70)), + Scale: util.Ptr(model.ScaleType(0)), + }), + IsSetpointChangeable: util.Ptr(true), + IsSetpointActive: util.Ptr(true), + } + + setpoints := &model.SetpointListDataType{ + SetpointData: []model.SetpointDataType{setpoint}, + } + + // Update the remote feature with the new setpoint data + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.cdtEntity, model.FeatureTypeTypeSetpoint, model.RoleTypeServer) + _, fErr := rFeature.UpdateData(true, model.FunctionTypeSetpointListData, setpoints, nil, nil) + assert.Nil(s.T(), fErr) + + // Test case: No mapping of operation modes to setpoints available + err = s.sut.WriteSetpoint(s.cdtEntity, model.HvacOperationModeTypeTypeOn, 35.0) + assert.NotNil(s.T(), err) + + // Create a mapping of operation modes to setpoints + s.sut.setpointsForOperationMode = map[model.HvacOperationModeTypeType]model.SetpointIdType{ + model.HvacOperationModeTypeTypeOn: *setpoint.SetpointId, + } + + // Test case: Setpoint and operation mode mapping available - the write should succeed + err = s.sut.WriteSetpoint(s.cdtEntity, model.HvacOperationModeTypeTypeOn, 35.0) + assert.Nil(s.T(), err) +} diff --git a/usecases/ca/cdt/testhelper_test.go b/usecases/ca/cdt/testhelper_test.go new file mode 100644 index 00000000..3b14f395 --- /dev/null +++ b/usecases/ca/cdt/testhelper_test.go @@ -0,0 +1,197 @@ +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" + "github.com/enbility/ship-go/util" + 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/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestCemCDTSuite(t *testing.T) { + suite.Run(t, new(CaCDTSuite)) +} + +type CaCDTSuite struct { + suite.Suite + + sut *CDT + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + cdtEntity spineapi.EntityRemoteInterface + + eventCalled bool +} + +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 + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeCEM}, + 9999, cert, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() + + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + s.sut = NewCDT(localEntity, s.Event) + s.sut.AddFeatures() + s.sut.AddUseCase() + + var entities []spineapi.EntityRemoteInterface + s.remoteDevice, entities = setupDevices(s.service, s.T()) + s.cdtEntity = entities[1] +} + +const remoteSki string = "testremoteski" + +func setupDevices( + eebusService api.ServiceInterface, + t *testing.T, +) ( + spineapi.DeviceRemoteInterface, + []spineapi.EntityRemoteInterface, +) { + localDevice := eebusService.LocalDevice() + + writeHandler := shipmocks.NewShipConnectionDataWriterInterface(t) + writeHandler.EXPECT().WriteShipMessageWithPayload(mock.Anything).Return().Maybe() + sender := spine.NewSender(writeHandler) + remoteDevice := spine.NewDeviceRemote(localDevice, remoteSki, sender) + + remoteDeviceName := "remote" + + var remoteFeatures = []struct { + featureType model.FeatureTypeType + supportedFcts []model.FunctionType + }{ + { + model.FeatureTypeTypeSetpoint, + []model.FunctionType{ + model.FunctionTypeSetpointDescriptionListData, + model.FunctionTypeSetpointConstraintsListData, + model.FunctionTypeSetpointListData, + }, + }, + { + model.FeatureTypeTypeHvac, + []model.FunctionType{ + 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{}, + }, + } + supportedFcts = append(supportedFcts, supportedFct) + } + + featureInformation := model.NodeManagementDetailedDiscoveryFeatureInformationType{ + Description: &model.NetworkManagementFeatureDescriptionDataType{ + FeatureAddress: &model.FeatureAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + Feature: util.Ptr(model.AddressFeatureType(index)), + }, + FeatureType: util.Ptr(feature.featureType), + Role: util.Ptr(model.RoleTypeServer), + SupportedFunction: supportedFcts, + }, + } + featureInformations = append(featureInformations, featureInformation) + } + + detailedData := &model.NodeManagementDetailedDiscoveryDataType{ + DeviceInformation: &model.NodeManagementDetailedDiscoveryDeviceInformationType{ + Description: &model.NetworkManagementDeviceDescriptionDataType{ + DeviceAddress: &model.DeviceAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + }, + }, + }, + EntityInformation: []model.NodeManagementDetailedDiscoveryEntityInformationType{ + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEVSE), + }, + }, + { + Description: &model.NetworkManagementEntityDescriptionDataType{ + EntityAddress: &model.EntityAddressType{ + Device: util.Ptr(model.AddressDeviceType(remoteDeviceName)), + Entity: []model.AddressEntityType{1, 1}, + }, + EntityType: util.Ptr(model.EntityTypeTypeEV), + }, + }, + }, + FeatureInformation: featureInformations, + } + + entities, err := remoteDevice.AddEntityAndFeatures(true, detailedData) + if err != nil { + fmt.Println(err) + } + remoteDevice.UpdateDevice(detailedData.DeviceInformation.Description) + + for _, entity := range entities { + entity.UpdateDeviceAddress(*remoteDevice.Address()) + } + + localDevice.AddRemoteDeviceForSki(remoteSki, remoteDevice) + + return remoteDevice, entities +} diff --git a/usecases/ca/cdt/types.go b/usecases/ca/cdt/types.go new file mode 100644 index 00000000..10c63992 --- /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" + + // Setpoints data updated + // + // Use `Setpoints` to get the current data + // + // Use Case CDT, Scenario 1 + DataUpdateSetpoints api.EventType = "ca-cdt-DataUpdateSetpoints" + + // Setpoint constraints data 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..68e1d48a --- /dev/null +++ b/usecases/ca/cdt/usecase.go @@ -0,0 +1,76 @@ +package cdt + +import ( + "github.com/enbility/eebus-go/api" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/usecase" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/spine" +) + +// Optimization of Heat Pump Compressor Function +type CDT struct { + *usecase.UseCaseBase + + setpointsForOperationMode map[model.HvacOperationModeTypeType]model.SetpointIdType +} + +var _ ucapi.CaCDTInterface = (*CDT)(nil) + +// NewCDT creates a new CDT use case +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, + ) + + uc := &CDT{ + UseCaseBase: usecase, + setpointsForOperationMode: make(map[model.HvacOperationModeTypeType]model.SetpointIdType), + } + + _ = spine.Events.Subscribe(uc) + + return uc +} + +// AddFeatures adds the features required for the CDT use case +func (e *CDT) AddFeatures() { + var clientFeatures = []model.FeatureTypeType{ + model.FeatureTypeTypeSetpoint, + model.FeatureTypeTypeHvac, + } + + for _, feature := range clientFeatures { + _ = e.LocalEntity.GetOrAddFeature(feature, model.RoleTypeClient) + } +} 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) +}