From fe8012efd743b7269257fa1c31c6e1e79d6d14ea Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Sat, 14 Dec 2024 14:33:23 +0100 Subject: [PATCH 01/11] First implementation of approval callback mechanism for failsafe limit writes. --- examples/hems/main.go | 16 +++++++-- usecases/api/cs_lpc.go | 11 +++++++ usecases/api/cs_lpp.go | 11 +++++++ usecases/cs/lpc/public.go | 33 +++++++++++++++++++ usecases/cs/lpc/usecase.go | 67 +++++++++++++++++++++++++++++++++++++- usecases/cs/lpp/public.go | 33 +++++++++++++++++++ usecases/cs/lpp/usecase.go | 67 +++++++++++++++++++++++++++++++++++++- 7 files changed, 233 insertions(+), 5 deletions(-) diff --git a/examples/hems/main.go b/examples/hems/main.go index 73ec2451..e13b2ccd 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -152,12 +152,17 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent case cslpc.WriteApprovalRequired: // get pending writes pendingWrites := h.uccslpc.PendingConsumptionLimits() + pendingFailsafeWrites := h.uccslpc.PendingFailsafeConsumptionLimits() // approve any write for msgCounter, write := range pendingWrites { - fmt.Println("Approving LPC write with msgCounter", msgCounter, "and limit", write.Value, "W") + fmt.Println("Approving LPC limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpc.ApproveOrDenyConsumptionLimit(msgCounter, true, "") } + for msgCounter, write := range pendingFailsafeWrites { + fmt.Println("Approving LPC failsafe limit write with msgCounter", msgCounter, "and limit", write.Value.ScaledNumber.GetValue()) + h.uccslpc.ApproveOrDenyFailsafeConsumptionLimit(msgCounter, true, "") + } case cslpc.DataUpdateLimit: if currentLimit, err := h.uccslpc.ConsumptionLimit(); err == nil { fmt.Println("New LPC Limit set to", currentLimit.Value, "W") @@ -172,12 +177,17 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent case cslpp.WriteApprovalRequired: // get pending writes pendingWrites := h.uccslpp.PendingProductionLimits() - + pendingFailsafeWrites := h.uccslpp.PendingFailsafeProductionLimits() + // approve any write for msgCounter, write := range pendingWrites { - fmt.Println("Approving LPP write with msgCounter", msgCounter, "and limit", write.Value, "W") + fmt.Println("Approving LPP limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpp.ApproveOrDenyProductionLimit(msgCounter, true, "") } + for msgCounter, write := range pendingFailsafeWrites { + fmt.Println("Approving LPP failsafe limit write with msgCounter", msgCounter, "and limit", write.Value.ScaledNumber.GetValue()) + h.uccslpp.ApproveOrDenyFailsafeProductionLimit(msgCounter, true, "") + } case cslpp.DataUpdateLimit: if currentLimit, err := h.uccslpp.ProductionLimit(); err == nil { fmt.Println("New LPP Limit set to", currentLimit.Value, "W") diff --git a/usecases/api/cs_lpc.go b/usecases/api/cs_lpc.go index 4b1d4eaa..1c64542e 100644 --- a/usecases/api/cs_lpc.go +++ b/usecases/api/cs_lpc.go @@ -72,6 +72,17 @@ type CsLPCInterface interface { // - changeable: boolean if the client service can change this value SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) + // return the currently pending incoming failsafe consumption write limits + PendingFailsafeConsumptionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType + + // accept or deny an incoming failsafe consumption write limit + // + // parameters: + // - msg: the incoming write message + // - approve: if the write limit for msg should be approved or not + // - reason: the reason why the approval is denied, otherwise an empty string + ApproveOrDenyFailsafeConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + // Scenario 3 // start sending heartbeat from the local entity supporting this usecase diff --git a/usecases/api/cs_lpp.go b/usecases/api/cs_lpp.go index 752c5c1e..888a04a9 100644 --- a/usecases/api/cs_lpp.go +++ b/usecases/api/cs_lpp.go @@ -72,6 +72,17 @@ type CsLPPInterface interface { // - changeable: boolean if the client service can change this value SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) + // return the currently pending incoming failsafe production write limits + PendingFailsafeProductionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType + + // accept or deny an incoming failsafe production write limit + // + // parameters: + // - msg: the incoming write message + // - approve: if the write limit for msg should be approved or not + // - reason: the reason why the approval is denied, otherwise an empty string + ApproveOrDenyFailsafeProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + // Scenario 3 // start sending heartbeat from the local entity supporting this usecase diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go index a2c5e000..6c24237a 100644 --- a/usecases/cs/lpc/public.go +++ b/usecases/cs/lpc/public.go @@ -270,6 +270,39 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool return dc.UpdateKeyValueDataForFilter(data, nil, filter) } +// return the currently pending incoming failsafe consumption write limits +func (e *LPC) PendingFailsafeConsumptionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType { + result := make(map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType) + + e.pendingFailsafeLimitMux.Lock() + defer e.pendingFailsafeLimitMux.Unlock() + + for key, msg := range e.pendingFailsafeLimits { + // The existance of this value is checked when it is added to the pendingFailsafeLimits map therefore we do not need to check it again + result[key] = msg.Cmd.DeviceConfigurationKeyValueListData.DeviceConfigurationKeyValueData[0] + } + + return result +} + +// accept or deny an incoming failsafe consumption write limit +// +// use PendingFailsafeConusmptionLimits to get the list of currently pending requests +func (e *LPC) ApproveOrDenyFailsafeConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingFailsafeLimitMux.Lock() + defer e.pendingFailsafeLimitMux.Unlock() + + msg, ok := e.pendingFailsafeLimits[msgCounter] + if !ok { + // no pending limit for this msgCounter, this is a caller error + return + } + + e.approveOrDenyFailsafeConsumptionLimit(msg, approve, reason) + + delete(e.pendingFailsafeLimits, msgCounter) +} + // Scenario 3 // start sending heartbeat from the local entity supporting this usecase diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index 197d27eb..180367fa 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -22,6 +22,9 @@ type LPC struct { pendingMux sync.Mutex pendingLimits map[model.MsgCounterType]*spineapi.Message + pendingFailsafeLimitMux sync.Mutex + pendingFailsafeLimits map[model.MsgCounterType]*spineapi.Message + heartbeatDiag *features.DeviceDiagnosis heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use @@ -79,6 +82,7 @@ func NewLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa uc := &LPC{ UseCaseBase: usecase, pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + pendingFailsafeLimits: make(map[model.MsgCounterType]*spineapi.Message), } _ = spine.Events.Subscribe(uc) @@ -175,6 +179,66 @@ func (e *LPC) loadControlWriteCB(msg *spineapi.Message) { go e.approveOrDenyConsumptionLimit(msg, true, "") } +func (e *LPC) approveOrDenyFailsafeConsumptionLimit(msg *spineapi.Message, approve bool, reason string) { + f := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + result := model.ErrorType{ + ErrorNumber: model.ErrorNumberType(0), + } + + if !approve { + result.ErrorNumber = model.ErrorNumberType(7) + result.Description = util.Ptr(model.DescriptionType(reason)) + } + f.ApproveOrDenyWrite(msg, result) +} + +// callback invoked on incoming write messages to this +// DeviceConfiguration server feature. +// the implementation only considers write messages for this use case and +// approves all others +func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.DeviceConfigurationKeyValueListData == nil { + logging.Log().Debug("LPC deviceConfigurationWriteCB: invalid message") + return + } + + data := msg.Cmd.DeviceConfigurationKeyValueListData + + if len(data.DeviceConfigurationKeyValueData) == 0 || data.DeviceConfigurationKeyValueData[0].KeyId == nil { + logging.Log().Debug("LPC deviceConfigurationWriteCB: no data") + return + } + + dcs, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return + } + + description, err := dcs.GetKeyValueDescriptionFoKeyId(*data.DeviceConfigurationKeyValueData[0].KeyId) + if description == nil || err != nil { + logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") + // if no description is found this write request is presumably for another usecase so we accept it + go e.approveOrDenyFailsafeConsumptionLimit(msg, true, "") + return + } + + switch *description.KeyName { + case model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: + e.pendingFailsafeLimitMux.Lock() + if _, ok := e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter] = msg + e.pendingFailsafeLimitMux.Unlock() + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return + } + default: + //approve because this is no request for the device configuration key this callback is responsible for + go e.approveOrDenyFailsafeConsumptionLimit(msg, true, "") + } +} + func (e *LPC) AddFeatures() { // client features _ = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) @@ -213,7 +277,8 @@ func (e *LPC) AddFeatures() { f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) - + _ = f.AddWriteApprovalCallback(e.deviceConfigurationWriteCB) + if dcs, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { dcs.AddKeyValueDescription( model.DeviceConfigurationKeyValueDescriptionDataType{ diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go index 89642395..ac623eee 100644 --- a/usecases/cs/lpp/public.go +++ b/usecases/cs/lpp/public.go @@ -272,6 +272,39 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool return dc.UpdateKeyValueDataForFilter(data, nil, filter) } +// return the currently pending incoming failsafe consumption write limits +func (e *LPP) PendingFailsafeProductionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType { + result := make(map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType) + + e.pendingFailsafeLimitMux.Lock() + defer e.pendingFailsafeLimitMux.Unlock() + + for key, msg := range e.pendingFailsafeLimits { + // The existance of this value is checked when it is added to the pendingFailsafeLimits map therefore we do not need to check it again + result[key] = msg.Cmd.DeviceConfigurationKeyValueListData.DeviceConfigurationKeyValueData[0] + } + + return result +} + +// accept or deny an incoming failsafe consumption write limit +// +// use PendingFailsafeConusmptionLimits to get the list of currently pending requests +func (e *LPP) ApproveOrDenyFailsafeProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingFailsafeLimitMux.Lock() + defer e.pendingFailsafeLimitMux.Unlock() + + msg, ok := e.pendingFailsafeLimits[msgCounter] + if !ok { + // no pending limit for this msgCounter, this is a caller error + return + } + + e.approveOrDenyFailsafeProductionLimit(msg, approve, reason) + + delete(e.pendingFailsafeLimits, msgCounter) +} + // Scenario 3 // start sending heartbeat from the local entity supporting this usecase diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index 30cd65c8..c91b5ecf 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -22,6 +22,9 @@ type LPP struct { pendingMux sync.Mutex pendingLimits map[model.MsgCounterType]*spineapi.Message + pendingFailsafeLimitMux sync.Mutex + pendingFailsafeLimits map[model.MsgCounterType]*spineapi.Message + heartbeatDiag *features.DeviceDiagnosis heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use @@ -29,7 +32,7 @@ type LPP struct { var _ ucapi.CsLPPInterface = (*LPP)(nil) -// Add support for the Limitation of Power Production (LPC) use case +// Add support for the Limitation of Power Production (LPP) use case // as a Controllable System actor // // Note: if the Monitoring of Power Consumption (MPC) or Monitoring of Grid Connection Point (MGCP) will be supported, add them first @@ -78,6 +81,7 @@ func NewLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa uc := &LPP{ UseCaseBase: usecase, pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + pendingFailsafeLimits: make(map[model.MsgCounterType]*spineapi.Message), } _ = spine.Events.Subscribe(uc) @@ -175,6 +179,66 @@ func (e *LPP) loadControlWriteCB(msg *spineapi.Message) { go e.approveOrDenyProductionLimit(msg, true, "") } +func (e *LPP) approveOrDenyFailsafeProductionLimit(msg *spineapi.Message, approve bool, reason string) { + f := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) + + result := model.ErrorType{ + ErrorNumber: model.ErrorNumberType(0), + } + + if !approve { + result.ErrorNumber = model.ErrorNumberType(7) + result.Description = util.Ptr(model.DescriptionType(reason)) + } + f.ApproveOrDenyWrite(msg, result) +} + +// callback invoked on incoming write messages to this +// DeviceConfiguration server feature. +// the implementation only considers write messages for this use case and +// approves all others +func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.DeviceConfigurationKeyValueListData == nil { + logging.Log().Debug("LPP deviceConfigurationWriteCB: invalid message") + return + } + + data := msg.Cmd.DeviceConfigurationKeyValueListData + + if len(data.DeviceConfigurationKeyValueData) == 0 || data.DeviceConfigurationKeyValueData[0].KeyId == nil { + logging.Log().Debug("LPP deviceConfigurationWriteCB: no data") + return + } + + dcs, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return + } + + description, err := dcs.GetKeyValueDescriptionFoKeyId(*data.DeviceConfigurationKeyValueData[0].KeyId) + if description == nil || err != nil { + logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") + // if no description is found this write request is presumably for another usecase so we accept it + go e.approveOrDenyFailsafeProductionLimit(msg, true, "") + return + } + + switch *description.KeyName { + case model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: + e.pendingFailsafeLimitMux.Lock() + if _, ok := e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter] = msg + e.pendingFailsafeLimitMux.Unlock() + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return + } + default: + //approve because this is no request for the device configuration key this callback is responsible for + go e.approveOrDenyFailsafeProductionLimit(msg, true, "") + } +} + func (e *LPP) AddFeatures() { // client features _ = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient) @@ -213,6 +277,7 @@ func (e *LPP) AddFeatures() { f = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) + _ = f.AddWriteApprovalCallback(e.deviceConfigurationWriteCB) if dcs, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { dcs.AddKeyValueDescription( From 599647dc2ab6d282334c96ddb7f10f306aa44f52 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Tue, 17 Dec 2024 17:37:23 +0100 Subject: [PATCH 02/11] Added FailsafeDurationMinimum to approval mechanism for device config feature. Made implementation of approval mechansim more general towards the device configuration feature. --- examples/hems/main.go | 30 +++++++++++++----- usecases/api/cs_lpc.go | 8 ++--- usecases/api/cs_lpp.go | 8 ++--- usecases/api/types.go | 8 +++++ usecases/cs/lpc/public.go | 35 +++++++++------------ usecases/cs/lpc/usecase.go | 62 +++++++++++++++++++++++++----------- usecases/cs/lpp/public.go | 35 +++++++++------------ usecases/cs/lpp/usecase.go | 64 ++++++++++++++++++++++++++------------ 8 files changed, 153 insertions(+), 97 deletions(-) diff --git a/examples/hems/main.go b/examples/hems/main.go index e13b2ccd..d545e7a4 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -152,16 +152,23 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent case cslpc.WriteApprovalRequired: // get pending writes pendingWrites := h.uccslpc.PendingConsumptionLimits() - pendingFailsafeWrites := h.uccslpc.PendingFailsafeConsumptionLimits() + pendingDeviceConfigWrites := h.uccslpc.PendingDeviceConfigurations() // approve any write for msgCounter, write := range pendingWrites { fmt.Println("Approving LPC limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpc.ApproveOrDenyConsumptionLimit(msgCounter, true, "") } - for msgCounter, write := range pendingFailsafeWrites { - fmt.Println("Approving LPC failsafe limit write with msgCounter", msgCounter, "and limit", write.Value.ScaledNumber.GetValue()) - h.uccslpc.ApproveOrDenyFailsafeConsumptionLimit(msgCounter, true, "") + for msgCounter, config := range pendingDeviceConfigWrites { + fmt.Print("Approving LPC device config write with msgCounter ", msgCounter) + if config.FailsafeDuration != nil { + fmt.Printf(" and FailsafeDurationMinimum %s", *config.FailsafeDuration) + } + if config.FailsafeLimit != nil { + fmt.Printf(" and FailsafeConsumptionLimit %f", *config.FailsafeLimit) + } + fmt.Print("\n") + h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") } case cslpc.DataUpdateLimit: if currentLimit, err := h.uccslpc.ConsumptionLimit(); err == nil { @@ -177,16 +184,23 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent case cslpp.WriteApprovalRequired: // get pending writes pendingWrites := h.uccslpp.PendingProductionLimits() - pendingFailsafeWrites := h.uccslpp.PendingFailsafeProductionLimits() + pendingDeviceConfigWrites := h.uccslpp.PendingDeviceConfigurations() // approve any write for msgCounter, write := range pendingWrites { fmt.Println("Approving LPP limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpp.ApproveOrDenyProductionLimit(msgCounter, true, "") } - for msgCounter, write := range pendingFailsafeWrites { - fmt.Println("Approving LPP failsafe limit write with msgCounter", msgCounter, "and limit", write.Value.ScaledNumber.GetValue()) - h.uccslpp.ApproveOrDenyFailsafeProductionLimit(msgCounter, true, "") + for msgCounter, config := range pendingDeviceConfigWrites { + fmt.Print("Approving LPP device config write with msgCounter ", msgCounter) + if config.FailsafeDuration != nil { + fmt.Printf(" and FailsafeDurationMinimum %s", *config.FailsafeDuration) + } + if config.FailsafeLimit != nil { + fmt.Printf(" and FailsafeProductionLimit %f", *config.FailsafeLimit) + } + fmt.Print("\n") + h.uccslpp.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") } case cslpp.DataUpdateLimit: if currentLimit, err := h.uccslpp.ProductionLimit(); err == nil { diff --git a/usecases/api/cs_lpc.go b/usecases/api/cs_lpc.go index 1c64542e..32860c98 100644 --- a/usecases/api/cs_lpc.go +++ b/usecases/api/cs_lpc.go @@ -72,16 +72,16 @@ type CsLPCInterface interface { // - changeable: boolean if the client service can change this value SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) - // return the currently pending incoming failsafe consumption write limits - PendingFailsafeConsumptionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType + // return the currently pending incoming device configuration writes + PendingDeviceConfigurations() map[model.MsgCounterType]*DeviceConfigurations - // accept or deny an incoming failsafe consumption write limit + // accept or deny an incoming device configuration writes // // parameters: // - msg: the incoming write message // - approve: if the write limit for msg should be approved or not // - reason: the reason why the approval is denied, otherwise an empty string - ApproveOrDenyFailsafeConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, approve bool, reason string) // Scenario 3 diff --git a/usecases/api/cs_lpp.go b/usecases/api/cs_lpp.go index 888a04a9..5c2f6156 100644 --- a/usecases/api/cs_lpp.go +++ b/usecases/api/cs_lpp.go @@ -72,16 +72,16 @@ type CsLPPInterface interface { // - changeable: boolean if the client service can change this value SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) - // return the currently pending incoming failsafe production write limits - PendingFailsafeProductionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType + // return the currently pending incoming device configuration writes + PendingDeviceConfigurations() map[model.MsgCounterType]*DeviceConfigurations - // accept or deny an incoming failsafe production write limit + // accept or deny an incoming device configuration writes // // parameters: // - msg: the incoming write message // - approve: if the write limit for msg should be approved or not // - reason: the reason why the approval is denied, otherwise an empty string - ApproveOrDenyFailsafeProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) + ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, approve bool, reason string) // Scenario 3 diff --git a/usecases/api/types.go b/usecases/api/types.go index 52a7f54e..7318a9ac 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -4,6 +4,7 @@ import ( "time" "github.com/enbility/spine-go/model" + spineapi "github.com/enbility/spine-go/api" ) type EVChargeStateType string @@ -166,3 +167,10 @@ type DurationSlotValue struct { Duration time.Duration // Duration of this slot Value float64 // Energy Cost or Power Limit } + +// Contains info on device configurations trying to be set via a write call on device configuration feature +type DeviceConfigurations struct { + FailsafeLimit *float64 + FailsafeDuration *time.Duration + Msg *spineapi.Message +} \ No newline at end of file diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go index 6c24237a..5eeeee7c 100644 --- a/usecases/cs/lpc/public.go +++ b/usecases/cs/lpc/public.go @@ -270,37 +270,30 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool return dc.UpdateKeyValueDataForFilter(data, nil, filter) } -// return the currently pending incoming failsafe consumption write limits -func (e *LPC) PendingFailsafeConsumptionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType { - result := make(map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType) - - e.pendingFailsafeLimitMux.Lock() - defer e.pendingFailsafeLimitMux.Unlock() - - for key, msg := range e.pendingFailsafeLimits { - // The existance of this value is checked when it is added to the pendingFailsafeLimits map therefore we do not need to check it again - result[key] = msg.Cmd.DeviceConfigurationKeyValueListData.DeviceConfigurationKeyValueData[0] - } - - return result +// return the currently pending incoming failsafe consumption limit writes +func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType]*ucapi.DeviceConfigurations { + e.pendingDeviceConfigMux.Lock() + defer e.pendingDeviceConfigMux.Unlock() + + return e.pendingDeviceConfigs } -// accept or deny an incoming failsafe consumption write limit +// accept or deny an incoming device configuration write // -// use PendingFailsafeConusmptionLimits to get the list of currently pending requests -func (e *LPC) ApproveOrDenyFailsafeConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { - e.pendingFailsafeLimitMux.Lock() - defer e.pendingFailsafeLimitMux.Unlock() +// use PendingDeviceConfigurations to get the list of currently pending requests +func (e *LPC) ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingDeviceConfigMux.Lock() + defer e.pendingDeviceConfigMux.Unlock() - msg, ok := e.pendingFailsafeLimits[msgCounter] + config, ok := e.pendingDeviceConfigs[msgCounter] if !ok { // no pending limit for this msgCounter, this is a caller error return } - e.approveOrDenyFailsafeConsumptionLimit(msg, approve, reason) + e.approveOrDenyDeviceConfiguration(config.Msg, approve, reason) - delete(e.pendingFailsafeLimits, msgCounter) + delete(e.pendingDeviceConfigs, msgCounter) } // Scenario 3 diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index 180367fa..1fc58c2c 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -2,6 +2,7 @@ package lpc import ( "sync" + "time" "github.com/enbility/eebus-go/api" features "github.com/enbility/eebus-go/features/client" @@ -22,8 +23,8 @@ type LPC struct { pendingMux sync.Mutex pendingLimits map[model.MsgCounterType]*spineapi.Message - pendingFailsafeLimitMux sync.Mutex - pendingFailsafeLimits map[model.MsgCounterType]*spineapi.Message + pendingDeviceConfigMux sync.Mutex + pendingDeviceConfigs map[model.MsgCounterType]*ucapi.DeviceConfigurations heartbeatDiag *features.DeviceDiagnosis @@ -82,7 +83,7 @@ func NewLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa uc := &LPC{ UseCaseBase: usecase, pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), - pendingFailsafeLimits: make(map[model.MsgCounterType]*spineapi.Message), + pendingDeviceConfigs: make(map[model.MsgCounterType]*ucapi.DeviceConfigurations), } _ = spine.Events.Subscribe(uc) @@ -179,7 +180,7 @@ func (e *LPC) loadControlWriteCB(msg *spineapi.Message) { go e.approveOrDenyConsumptionLimit(msg, true, "") } -func (e *LPC) approveOrDenyFailsafeConsumptionLimit(msg *spineapi.Message, approve bool, reason string) { +func (e *LPC) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bool, reason string) { f := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) result := model.ErrorType{ @@ -190,6 +191,7 @@ func (e *LPC) approveOrDenyFailsafeConsumptionLimit(msg *spineapi.Message, appro result.ErrorNumber = model.ErrorNumberType(7) result.Description = util.Ptr(model.DescriptionType(reason)) } + f.ApproveOrDenyWrite(msg, result) } @@ -216,26 +218,48 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { return } - description, err := dcs.GetKeyValueDescriptionFoKeyId(*data.DeviceConfigurationKeyValueData[0].KeyId) - if description == nil || err != nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") - // if no description is found this write request is presumably for another usecase so we accept it - go e.approveOrDenyFailsafeConsumptionLimit(msg, true, "") - return + var failsafeLimit *float64 = nil + var failsafeDuration *time.Duration = nil + for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { + + description, err := dcs.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) + if description == nil || err != nil { + logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") + // if no description is found this write request is presumably for another usecase + continue + } + + // We only care about new values for either FailsafeConsumptionActivePowerLimit or FailsafeDurationMinimum, all other keys are ignored + switch *description.KeyName { + case model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: + value := deviceKeyValueData.Value.ScaledNumber.GetValue() + failsafeLimit = &value + case model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: + value, err := deviceKeyValueData.Value.Duration.GetTimeDuration() + if err == nil { + failsafeDuration = &value + } else { + logging.Log().Debug("LPC deviceConfigurationWriteCB: received invalid or no value as duration while trying to set FailsafeDurationMinimum") + } + } } - switch *description.KeyName { - case model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: - e.pendingFailsafeLimitMux.Lock() - if _, ok := e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter]; !ok { - e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter] = msg - e.pendingFailsafeLimitMux.Unlock() + if failsafeDuration != nil || failsafeLimit != nil { + e.pendingDeviceConfigMux.Lock() + if _, ok := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = &ucapi.DeviceConfigurations{ + FailsafeLimit: failsafeLimit, + FailsafeDuration: failsafeDuration, + Msg: msg, + } + e.pendingDeviceConfigMux.Unlock() e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) return } - default: - //approve because this is no request for the device configuration key this callback is responsible for - go e.approveOrDenyFailsafeConsumptionLimit(msg, true, "") + e.pendingDeviceConfigMux.Unlock() + } else { + // As neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + e.approveOrDenyDeviceConfiguration(msg, true, "") } } diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go index ac623eee..294febd4 100644 --- a/usecases/cs/lpp/public.go +++ b/usecases/cs/lpp/public.go @@ -272,37 +272,30 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool return dc.UpdateKeyValueDataForFilter(data, nil, filter) } -// return the currently pending incoming failsafe consumption write limits -func (e *LPP) PendingFailsafeProductionLimits() map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType { - result := make(map[model.MsgCounterType]model.DeviceConfigurationKeyValueDataType) - - e.pendingFailsafeLimitMux.Lock() - defer e.pendingFailsafeLimitMux.Unlock() - - for key, msg := range e.pendingFailsafeLimits { - // The existance of this value is checked when it is added to the pendingFailsafeLimits map therefore we do not need to check it again - result[key] = msg.Cmd.DeviceConfigurationKeyValueListData.DeviceConfigurationKeyValueData[0] - } - - return result +// return the currently pending incoming failsafe consumption limit writes +func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType]*ucapi.DeviceConfigurations { + e.pendingDeviceConfigMux.Lock() + defer e.pendingDeviceConfigMux.Unlock() + + return e.pendingDeviceConfigs } -// accept or deny an incoming failsafe consumption write limit +// accept or deny an incoming device configuration write // -// use PendingFailsafeConusmptionLimits to get the list of currently pending requests -func (e *LPP) ApproveOrDenyFailsafeProductionLimit(msgCounter model.MsgCounterType, approve bool, reason string) { - e.pendingFailsafeLimitMux.Lock() - defer e.pendingFailsafeLimitMux.Unlock() +// use PendingDeviceConfigurations to get the list of currently pending requests +func (e *LPP) ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, approve bool, reason string) { + e.pendingDeviceConfigMux.Lock() + defer e.pendingDeviceConfigMux.Unlock() - msg, ok := e.pendingFailsafeLimits[msgCounter] + config, ok := e.pendingDeviceConfigs[msgCounter] if !ok { // no pending limit for this msgCounter, this is a caller error return } - e.approveOrDenyFailsafeProductionLimit(msg, approve, reason) + e.approveOrDenyDeviceConfiguration(config.Msg, approve, reason) - delete(e.pendingFailsafeLimits, msgCounter) + delete(e.pendingDeviceConfigs, msgCounter) } // Scenario 3 diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index c91b5ecf..968d0933 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -2,6 +2,7 @@ package lpp import ( "sync" + "time" "github.com/enbility/eebus-go/api" features "github.com/enbility/eebus-go/features/client" @@ -22,8 +23,8 @@ type LPP struct { pendingMux sync.Mutex pendingLimits map[model.MsgCounterType]*spineapi.Message - pendingFailsafeLimitMux sync.Mutex - pendingFailsafeLimits map[model.MsgCounterType]*spineapi.Message + pendingDeviceConfigMux sync.Mutex + pendingDeviceConfigs map[model.MsgCounterType]*ucapi.DeviceConfigurations heartbeatDiag *features.DeviceDiagnosis @@ -81,7 +82,7 @@ func NewLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa uc := &LPP{ UseCaseBase: usecase, pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), - pendingFailsafeLimits: make(map[model.MsgCounterType]*spineapi.Message), + pendingDeviceConfigs: make(map[model.MsgCounterType]*ucapi.DeviceConfigurations), } _ = spine.Events.Subscribe(uc) @@ -179,7 +180,7 @@ func (e *LPP) loadControlWriteCB(msg *spineapi.Message) { go e.approveOrDenyProductionLimit(msg, true, "") } -func (e *LPP) approveOrDenyFailsafeProductionLimit(msg *spineapi.Message, approve bool, reason string) { +func (e *LPP) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bool, reason string) { f := e.LocalEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeDeviceConfiguration, model.RoleTypeServer) result := model.ErrorType{ @@ -190,6 +191,7 @@ func (e *LPP) approveOrDenyFailsafeProductionLimit(msg *spineapi.Message, approv result.ErrorNumber = model.ErrorNumberType(7) result.Description = util.Ptr(model.DescriptionType(reason)) } + f.ApproveOrDenyWrite(msg, result) } @@ -216,28 +218,50 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { return } - description, err := dcs.GetKeyValueDescriptionFoKeyId(*data.DeviceConfigurationKeyValueData[0].KeyId) - if description == nil || err != nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") - // if no description is found this write request is presumably for another usecase so we accept it - go e.approveOrDenyFailsafeProductionLimit(msg, true, "") - return + var failsafeLimit *float64 = nil + var failsafeDuration *time.Duration = nil + for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { + + description, err := dcs.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) + if description == nil || err != nil { + logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") + // if no description is found this write request is presumably for another usecase + continue + } + + // We only care about new values for either FailsafeProductionActivePowerLimit or FailsafeDurationMinimum, all other keys are ignored + switch *description.KeyName { + case model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: + value := deviceKeyValueData.Value.ScaledNumber.GetValue() + failsafeLimit = &value + case model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: + value, err := deviceKeyValueData.Value.Duration.GetTimeDuration() + if err == nil { + failsafeDuration = &value + } else { + logging.Log().Debug("LPP deviceConfigurationWriteCB: received invalid or no value as duration while trying to set FailsafeDurationMinimum") + } + } } - switch *description.KeyName { - case model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: - e.pendingFailsafeLimitMux.Lock() - if _, ok := e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter]; !ok { - e.pendingFailsafeLimits[*msg.RequestHeader.MsgCounter] = msg - e.pendingFailsafeLimitMux.Unlock() + if failsafeDuration != nil || failsafeLimit != nil { + e.pendingDeviceConfigMux.Lock() + if _, ok := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !ok { + e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = &ucapi.DeviceConfigurations{ + FailsafeLimit: failsafeLimit, + FailsafeDuration: failsafeDuration, + Msg: msg, + } + e.pendingDeviceConfigMux.Unlock() e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) return } - default: - //approve because this is no request for the device configuration key this callback is responsible for - go e.approveOrDenyFailsafeProductionLimit(msg, true, "") + e.pendingDeviceConfigMux.Unlock() + } else { + // As neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + e.approveOrDenyDeviceConfiguration(msg, true, "") } -} +} func (e *LPP) AddFeatures() { // client features From 298a6db412ce9c97d814dcca2b195565f980ec57 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Thu, 19 Dec 2024 09:24:36 +0100 Subject: [PATCH 03/11] refined some comments and a variable name --- usecases/cs/lpc/usecase.go | 10 +++++----- usecases/cs/lpp/usecase.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index 1fc58c2c..ec8b2ead 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -213,7 +213,7 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { return } - dcs, err := server.NewDeviceConfiguration(e.LocalEntity) + dc, err := server.NewDeviceConfiguration(e.LocalEntity) if err != nil { return } @@ -222,10 +222,9 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { var failsafeDuration *time.Duration = nil for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dcs.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) + description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) if description == nil || err != nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") - // if no description is found this write request is presumably for another usecase + logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found") continue } @@ -244,6 +243,7 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { } } + // Only ask for write approval if at least one of the configurations we care about is trying to be set if failsafeDuration != nil || failsafeLimit != nil { e.pendingDeviceConfigMux.Lock() if _, ok := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !ok { @@ -258,7 +258,7 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { } e.pendingDeviceConfigMux.Unlock() } else { - // As neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept e.approveOrDenyDeviceConfiguration(msg, true, "") } } diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index 968d0933..57cecdb0 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -213,7 +213,7 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { return } - dcs, err := server.NewDeviceConfiguration(e.LocalEntity) + dc, err := server.NewDeviceConfiguration(e.LocalEntity) if err != nil { return } @@ -222,10 +222,9 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { var failsafeDuration *time.Duration = nil for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dcs.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) + description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) if description == nil || err != nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found on this usecase, possibly write message for other usecase") - // if no description is found this write request is presumably for another usecase + logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found") continue } @@ -244,6 +243,7 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { } } + // Only ask for write approval if at least one of the configurations we care about is trying to be set if failsafeDuration != nil || failsafeLimit != nil { e.pendingDeviceConfigMux.Lock() if _, ok := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !ok { @@ -258,7 +258,7 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { } e.pendingDeviceConfigMux.Unlock() } else { - // As neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept e.approveOrDenyDeviceConfiguration(msg, true, "") } } From 5b60f495467ae81853fe9a0111d16a475fc6fa77 Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Thu, 16 Jan 2025 14:35:34 +0100 Subject: [PATCH 04/11] Provide a more uniform interface for retrieving pending writes --- examples/hems/main.go | 47 ++++++++++++++++++++++++--------- usecases/api/cs_lpc.go | 2 +- usecases/api/cs_lpp.go | 2 +- usecases/api/types.go | 8 ------ usecases/cs/lpc/public.go | 24 ++++++++++++----- usecases/cs/lpc/usecase.go | 54 +++++++++++++------------------------- usecases/cs/lpp/public.go | 23 ++++++++++++---- usecases/cs/lpp/usecase.go | 54 +++++++++++++------------------------- 8 files changed, 108 insertions(+), 106 deletions(-) diff --git a/examples/hems/main.go b/examples/hems/main.go index d545e7a4..e38a7ba4 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -14,6 +14,7 @@ import ( "time" "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/features/server" "github.com/enbility/eebus-go/service" ucapi "github.com/enbility/eebus-go/usecases/api" "github.com/enbility/eebus-go/usecases/cem/vabd" @@ -159,13 +160,23 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent fmt.Println("Approving LPC limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpc.ApproveOrDenyConsumptionLimit(msgCounter, true, "") } - for msgCounter, config := range pendingDeviceConfigWrites { - fmt.Print("Approving LPC device config write with msgCounter ", msgCounter) - if config.FailsafeDuration != nil { - fmt.Printf(" and FailsafeDurationMinimum %s", *config.FailsafeDuration) + for msgCounter, configs := range pendingDeviceConfigWrites { + localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + dc, err := server.NewDeviceConfiguration(localEntity) + if err != nil { + fmt.Println("Not approving LPC device configuration writes because of error:") + fmt.Println(err) + return } - if config.FailsafeLimit != nil { - fmt.Printf(" and FailsafeConsumptionLimit %f", *config.FailsafeLimit) + fmt.Printf("Approving LPC device config write with msgCounter %d ", msgCounter) + for _, config := range(configs) { + description, err := dc.GetKeyValueDescriptionFoKeyId(*config.KeyId) + if description == nil || err != nil { + fmt.Printf("LPC approving device configuation writes: no device configuration for KeyID %d found\n", *config.KeyId) + continue + } + + fmt.Printf("including %s ", *description.KeyName) } fmt.Print("\n") h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") @@ -191,16 +202,26 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent fmt.Println("Approving LPP limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpp.ApproveOrDenyProductionLimit(msgCounter, true, "") } - for msgCounter, config := range pendingDeviceConfigWrites { - fmt.Print("Approving LPP device config write with msgCounter ", msgCounter) - if config.FailsafeDuration != nil { - fmt.Printf(" and FailsafeDurationMinimum %s", *config.FailsafeDuration) + for msgCounter, configs := range pendingDeviceConfigWrites { + localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeCEM) + dc, err := server.NewDeviceConfiguration(localEntity) + if err != nil { + fmt.Println("Not approving LPC device configuration writes because of error:") + fmt.Println(err) + return } - if config.FailsafeLimit != nil { - fmt.Printf(" and FailsafeProductionLimit %f", *config.FailsafeLimit) + + fmt.Printf("Approving LPP device config write with msgCounter %d ", msgCounter) + for _, config := range(configs) { + description, err := dc.GetKeyValueDescriptionFoKeyId(*config.KeyId) + if description == nil || err != nil { + fmt.Printf("LPP approving device configuation writes: no device configuration for KeyID %d found\n", *config.KeyId) + continue + } + fmt.Printf("including %s ", *description.KeyName) } fmt.Print("\n") - h.uccslpp.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") + h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") } case cslpp.DataUpdateLimit: if currentLimit, err := h.uccslpp.ProductionLimit(); err == nil { diff --git a/usecases/api/cs_lpc.go b/usecases/api/cs_lpc.go index 32860c98..fd2bf833 100644 --- a/usecases/api/cs_lpc.go +++ b/usecases/api/cs_lpc.go @@ -73,7 +73,7 @@ type CsLPCInterface interface { SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) // return the currently pending incoming device configuration writes - PendingDeviceConfigurations() map[model.MsgCounterType]*DeviceConfigurations + PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType // accept or deny an incoming device configuration writes // diff --git a/usecases/api/cs_lpp.go b/usecases/api/cs_lpp.go index 5c2f6156..3dfe0981 100644 --- a/usecases/api/cs_lpp.go +++ b/usecases/api/cs_lpp.go @@ -73,7 +73,7 @@ type CsLPPInterface interface { SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) // return the currently pending incoming device configuration writes - PendingDeviceConfigurations() map[model.MsgCounterType]*DeviceConfigurations + PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType // accept or deny an incoming device configuration writes // diff --git a/usecases/api/types.go b/usecases/api/types.go index 7318a9ac..4f758b68 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -4,7 +4,6 @@ import ( "time" "github.com/enbility/spine-go/model" - spineapi "github.com/enbility/spine-go/api" ) type EVChargeStateType string @@ -166,11 +165,4 @@ type IncentiveTariffDescription struct { type DurationSlotValue struct { Duration time.Duration // Duration of this slot Value float64 // Energy Cost or Power Limit -} - -// Contains info on device configurations trying to be set via a write call on device configuration feature -type DeviceConfigurations struct { - FailsafeLimit *float64 - FailsafeDuration *time.Duration - Msg *spineapi.Message } \ No newline at end of file diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go index 5eeeee7c..c7497fac 100644 --- a/usecases/cs/lpc/public.go +++ b/usecases/cs/lpc/public.go @@ -271,11 +271,24 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool } // return the currently pending incoming failsafe consumption limit writes -func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType]*ucapi.DeviceConfigurations { +func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType { + result := make(map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType) + e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() - - return e.pendingDeviceConfigs + + for msgCounter, msg := range e.pendingDeviceConfigs { + data := msg.Cmd.DeviceConfigurationKeyValueListData + for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { + if _, exists := result[msgCounter]; exists { + result[msgCounter] = append(result[msgCounter], configKeyValueData) + } else { + result[msgCounter] = []model.DeviceConfigurationKeyValueDataType{configKeyValueData} + } + } + } + + return result } // accept or deny an incoming device configuration write @@ -285,14 +298,13 @@ func (e *LPC) ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() - config, ok := e.pendingDeviceConfigs[msgCounter] + msg, ok := e.pendingDeviceConfigs[msgCounter] if !ok { // no pending limit for this msgCounter, this is a caller error return } - e.approveOrDenyDeviceConfiguration(config.Msg, approve, reason) - + e.approveOrDenyDeviceConfiguration(msg, approve, reason) delete(e.pendingDeviceConfigs, msgCounter) } diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index ec8b2ead..06f16b65 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -2,7 +2,6 @@ package lpc import ( "sync" - "time" "github.com/enbility/eebus-go/api" features "github.com/enbility/eebus-go/features/client" @@ -24,7 +23,7 @@ type LPC struct { pendingLimits map[model.MsgCounterType]*spineapi.Message pendingDeviceConfigMux sync.Mutex - pendingDeviceConfigs map[model.MsgCounterType]*ucapi.DeviceConfigurations + pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message heartbeatDiag *features.DeviceDiagnosis @@ -83,7 +82,7 @@ func NewLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa uc := &LPC{ UseCaseBase: usecase, pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), - pendingDeviceConfigs: make(map[model.MsgCounterType]*ucapi.DeviceConfigurations), + pendingDeviceConfigs: make(map[model.MsgCounterType]*spineapi.Message), } _ = spine.Events.Subscribe(uc) @@ -218,49 +217,32 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { return } - var failsafeLimit *float64 = nil - var failsafeDuration *time.Duration = nil + configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ + model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: {}, + model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, + } for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) if description == nil || err != nil { logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found") continue } - // We only care about new values for either FailsafeConsumptionActivePowerLimit or FailsafeDurationMinimum, all other keys are ignored - switch *description.KeyName { - case model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: - value := deviceKeyValueData.Value.ScaledNumber.GetValue() - failsafeLimit = &value - case model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: - value, err := deviceKeyValueData.Value.Duration.GetTimeDuration() - if err == nil { - failsafeDuration = &value - } else { - logging.Log().Debug("LPC deviceConfigurationWriteCB: received invalid or no value as duration while trying to set FailsafeDurationMinimum") - } - } - } - - // Only ask for write approval if at least one of the configurations we care about is trying to be set - if failsafeDuration != nil || failsafeLimit != nil { - e.pendingDeviceConfigMux.Lock() - if _, ok := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !ok { - e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = &ucapi.DeviceConfigurations{ - FailsafeLimit: failsafeLimit, - FailsafeDuration: failsafeDuration, - Msg: msg, + // Only ask for write approval if at least one of the configurations we care about is trying to be set + if _, exists := configsToApprove[*description.KeyName]; exists { + e.pendingDeviceConfigMux.Lock() + if _, exists := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !exists { + e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = msg + e.pendingDeviceConfigMux.Unlock() + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return } e.pendingDeviceConfigMux.Unlock() - e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) - return - } - e.pendingDeviceConfigMux.Unlock() - } else { - // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept - e.approveOrDenyDeviceConfiguration(msg, true, "") + } } + + // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + e.approveOrDenyDeviceConfiguration(msg, true, "") } func (e *LPC) AddFeatures() { diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go index 294febd4..a7546fdd 100644 --- a/usecases/cs/lpp/public.go +++ b/usecases/cs/lpp/public.go @@ -273,11 +273,24 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool } // return the currently pending incoming failsafe consumption limit writes -func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType]*ucapi.DeviceConfigurations { +func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType { + result := make(map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType) + e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() - - return e.pendingDeviceConfigs + + for msgCounter, msg := range e.pendingDeviceConfigs { + data := msg.Cmd.DeviceConfigurationKeyValueListData + for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { + if _, exists := result[msgCounter]; exists { + result[msgCounter] = append(result[msgCounter], configKeyValueData) + } else { + result[msgCounter] = []model.DeviceConfigurationKeyValueDataType{configKeyValueData} + } + } + } + + return result } // accept or deny an incoming device configuration write @@ -287,13 +300,13 @@ func (e *LPP) ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() - config, ok := e.pendingDeviceConfigs[msgCounter] + msg, ok := e.pendingDeviceConfigs[msgCounter] if !ok { // no pending limit for this msgCounter, this is a caller error return } - e.approveOrDenyDeviceConfiguration(config.Msg, approve, reason) + e.approveOrDenyDeviceConfiguration(msg, approve, reason) delete(e.pendingDeviceConfigs, msgCounter) } diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index 57cecdb0..62ca32e4 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -2,7 +2,6 @@ package lpp import ( "sync" - "time" "github.com/enbility/eebus-go/api" features "github.com/enbility/eebus-go/features/client" @@ -24,7 +23,7 @@ type LPP struct { pendingLimits map[model.MsgCounterType]*spineapi.Message pendingDeviceConfigMux sync.Mutex - pendingDeviceConfigs map[model.MsgCounterType]*ucapi.DeviceConfigurations + pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message heartbeatDiag *features.DeviceDiagnosis @@ -82,7 +81,7 @@ func NewLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa uc := &LPP{ UseCaseBase: usecase, pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), - pendingDeviceConfigs: make(map[model.MsgCounterType]*ucapi.DeviceConfigurations), + pendingDeviceConfigs: make(map[model.MsgCounterType]*spineapi.Message), } _ = spine.Events.Subscribe(uc) @@ -217,9 +216,10 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { if err != nil { return } - - var failsafeLimit *float64 = nil - var failsafeDuration *time.Duration = nil + configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ + model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: {}, + model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, + } for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) @@ -228,39 +228,21 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { continue } - // We only care about new values for either FailsafeProductionActivePowerLimit or FailsafeDurationMinimum, all other keys are ignored - switch *description.KeyName { - case model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: - value := deviceKeyValueData.Value.ScaledNumber.GetValue() - failsafeLimit = &value - case model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: - value, err := deviceKeyValueData.Value.Duration.GetTimeDuration() - if err == nil { - failsafeDuration = &value - } else { - logging.Log().Debug("LPP deviceConfigurationWriteCB: received invalid or no value as duration while trying to set FailsafeDurationMinimum") - } - } - } - - // Only ask for write approval if at least one of the configurations we care about is trying to be set - if failsafeDuration != nil || failsafeLimit != nil { - e.pendingDeviceConfigMux.Lock() - if _, ok := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !ok { - e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = &ucapi.DeviceConfigurations{ - FailsafeLimit: failsafeLimit, - FailsafeDuration: failsafeDuration, - Msg: msg, + // Only ask for write approval if at least one of the configurations we care about is trying to be set + if _, exists := configsToApprove[*description.KeyName]; exists { + e.pendingDeviceConfigMux.Lock() + if _, exists := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !exists { + e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = msg + e.pendingDeviceConfigMux.Unlock() + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + return } e.pendingDeviceConfigMux.Unlock() - e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) - return - } - e.pendingDeviceConfigMux.Unlock() - } else { - // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept - e.approveOrDenyDeviceConfiguration(msg, true, "") + } } + + // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + e.approveOrDenyDeviceConfiguration(msg, true, "") } func (e *LPP) AddFeatures() { From ad7f2915da99af93b6b06d03d41c42adc0acd0ac Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Thu, 16 Jan 2025 15:43:50 +0100 Subject: [PATCH 05/11] Add config's full description to the PendingDeviceConfiguration return This allows to more easily check for what a new value is trying to be set and what kind of value is being set (i.e. a duration, a bool, string etc.) --- examples/hems/main.go | 39 ++++++--------------------------------- usecases/api/cs_lpc.go | 2 +- usecases/api/cs_lpp.go | 2 +- usecases/api/types.go | 6 ++++++ usecases/cs/lpc/public.go | 27 +++++++++++++++++++++------ usecases/cs/lpp/public.go | 27 +++++++++++++++++++++------ 6 files changed, 56 insertions(+), 47 deletions(-) diff --git a/examples/hems/main.go b/examples/hems/main.go index e38a7ba4..b3088467 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -14,7 +14,6 @@ import ( "time" "github.com/enbility/eebus-go/api" - "github.com/enbility/eebus-go/features/server" "github.com/enbility/eebus-go/service" ucapi "github.com/enbility/eebus-go/usecases/api" "github.com/enbility/eebus-go/usecases/cem/vabd" @@ -94,7 +93,7 @@ func (h *hems) run() { configuration.SetAlternateIdentifier("Demo-HEMS-123456789") h.myService = service.NewService(configuration, h) - h.myService.SetLogging(h) + //h.myService.SetLogging(h) if err = h.myService.Setup(); err != nil { fmt.Println(err) @@ -161,22 +160,9 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent h.uccslpc.ApproveOrDenyConsumptionLimit(msgCounter, true, "") } for msgCounter, configs := range pendingDeviceConfigWrites { - localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeCEM) - dc, err := server.NewDeviceConfiguration(localEntity) - if err != nil { - fmt.Println("Not approving LPC device configuration writes because of error:") - fmt.Println(err) - return - } - fmt.Printf("Approving LPC device config write with msgCounter %d ", msgCounter) + fmt.Printf("Approving LPC device config write with msgCounter %d for features: ", msgCounter) for _, config := range(configs) { - description, err := dc.GetKeyValueDescriptionFoKeyId(*config.KeyId) - if description == nil || err != nil { - fmt.Printf("LPC approving device configuation writes: no device configuration for KeyID %d found\n", *config.KeyId) - continue - } - - fmt.Printf("including %s ", *description.KeyName) + fmt.Printf("%s ", *config.Description.KeyName) } fmt.Print("\n") h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") @@ -203,25 +189,12 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent h.uccslpp.ApproveOrDenyProductionLimit(msgCounter, true, "") } for msgCounter, configs := range pendingDeviceConfigWrites { - localEntity := h.myService.LocalDevice().EntityForType(model.EntityTypeTypeCEM) - dc, err := server.NewDeviceConfiguration(localEntity) - if err != nil { - fmt.Println("Not approving LPC device configuration writes because of error:") - fmt.Println(err) - return - } - - fmt.Printf("Approving LPP device config write with msgCounter %d ", msgCounter) + fmt.Printf("Approving LPP device config write with msgCounter %d for features: ", msgCounter) for _, config := range(configs) { - description, err := dc.GetKeyValueDescriptionFoKeyId(*config.KeyId) - if description == nil || err != nil { - fmt.Printf("LPP approving device configuation writes: no device configuration for KeyID %d found\n", *config.KeyId) - continue - } - fmt.Printf("including %s ", *description.KeyName) + fmt.Printf("%s ", *config.Description.KeyName) } fmt.Print("\n") - h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") + h.uccslpp.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") } case cslpp.DataUpdateLimit: if currentLimit, err := h.uccslpp.ProductionLimit(); err == nil { diff --git a/usecases/api/cs_lpc.go b/usecases/api/cs_lpc.go index fd2bf833..fc1b7914 100644 --- a/usecases/api/cs_lpc.go +++ b/usecases/api/cs_lpc.go @@ -73,7 +73,7 @@ type CsLPCInterface interface { SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) // return the currently pending incoming device configuration writes - PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType + PendingDeviceConfigurations() map[model.MsgCounterType][]PendingDeviceConfiguration // accept or deny an incoming device configuration writes // diff --git a/usecases/api/cs_lpp.go b/usecases/api/cs_lpp.go index 3dfe0981..b436bb02 100644 --- a/usecases/api/cs_lpp.go +++ b/usecases/api/cs_lpp.go @@ -73,7 +73,7 @@ type CsLPPInterface interface { SetFailsafeDurationMinimum(duration time.Duration, changeable bool) (resultErr error) // return the currently pending incoming device configuration writes - PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType + PendingDeviceConfigurations() map[model.MsgCounterType][]PendingDeviceConfiguration // accept or deny an incoming device configuration writes // diff --git a/usecases/api/types.go b/usecases/api/types.go index 4f758b68..cba0c7c0 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -165,4 +165,10 @@ type IncentiveTariffDescription struct { type DurationSlotValue struct { Duration time.Duration // Duration of this slot Value float64 // Energy Cost or Power Limit +} + +type PendingDeviceConfiguration struct { + Description *model.DeviceConfigurationKeyValueDescriptionDataType `json:"description,omitempty"` + Value *model.DeviceConfigurationKeyValueValueType `json:"value,omitempty"` + IsValueChangeable *bool `json:"isValueChangeable,omitempty" eebus:"writecheck"` } \ No newline at end of file diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go index c7497fac..e6fce4eb 100644 --- a/usecases/cs/lpc/public.go +++ b/usecases/cs/lpc/public.go @@ -271,23 +271,38 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool } // return the currently pending incoming failsafe consumption limit writes -func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType { - result := make(map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType) +func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { + result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return result + } + for msgCounter, msg := range e.pendingDeviceConfigs { data := msg.Cmd.DeviceConfigurationKeyValueListData for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { - if _, exists := result[msgCounter]; exists { - result[msgCounter] = append(result[msgCounter], configKeyValueData) + description, err := dc.GetKeyValueDescriptionFoKeyId(*configKeyValueData.KeyId) + if err != nil { + continue + } + + pendingConfigData := ucapi.PendingDeviceConfiguration{ + Description: description, + Value: configKeyValueData.Value, + IsValueChangeable: configKeyValueData.IsValueChangeable, + } + + if _, exists := result[msgCounter]; !exists { + result[msgCounter] = []ucapi.PendingDeviceConfiguration{pendingConfigData} } else { - result[msgCounter] = []model.DeviceConfigurationKeyValueDataType{configKeyValueData} + result[msgCounter] = append(result[msgCounter], pendingConfigData) } } } - return result } diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go index a7546fdd..36a779f8 100644 --- a/usecases/cs/lpp/public.go +++ b/usecases/cs/lpp/public.go @@ -273,23 +273,38 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool } // return the currently pending incoming failsafe consumption limit writes -func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType { - result := make(map[model.MsgCounterType][]model.DeviceConfigurationKeyValueDataType) +func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { + result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() + dc, err := server.NewDeviceConfiguration(e.LocalEntity) + if err != nil { + return result + } + for msgCounter, msg := range e.pendingDeviceConfigs { data := msg.Cmd.DeviceConfigurationKeyValueListData for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { - if _, exists := result[msgCounter]; exists { - result[msgCounter] = append(result[msgCounter], configKeyValueData) + description, err := dc.GetKeyValueDescriptionFoKeyId(*configKeyValueData.KeyId) + if err != nil { + continue + } + + pendingConfigData := ucapi.PendingDeviceConfiguration{ + Description: description, + Value: configKeyValueData.Value, + IsValueChangeable: configKeyValueData.IsValueChangeable, + } + + if _, exists := result[msgCounter]; !exists { + result[msgCounter] = []ucapi.PendingDeviceConfiguration{pendingConfigData} } else { - result[msgCounter] = []model.DeviceConfigurationKeyValueDataType{configKeyValueData} + result[msgCounter] = append(result[msgCounter], pendingConfigData) } } } - return result } From 98da07a77b7511ac73f1eb0d361aed1ba18238e7 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Thu, 13 Feb 2025 11:20:23 +0100 Subject: [PATCH 06/11] Reformat files and fix some inconsistencies --- examples/hems/main.go | 8 ++++---- usecases/api/types.go | 8 ++++---- usecases/cs/lpc/public.go | 8 ++++---- usecases/cs/lpc/usecase.go | 37 +++++++++++++++++++++++-------------- usecases/cs/lpp/public.go | 8 ++++---- usecases/cs/lpp/usecase.go | 37 +++++++++++++++++++++++-------------- 6 files changed, 62 insertions(+), 44 deletions(-) diff --git a/examples/hems/main.go b/examples/hems/main.go index b3088467..a4e16f1d 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -93,7 +93,7 @@ func (h *hems) run() { configuration.SetAlternateIdentifier("Demo-HEMS-123456789") h.myService = service.NewService(configuration, h) - //h.myService.SetLogging(h) + h.myService.SetLogging(h) if err = h.myService.Setup(); err != nil { fmt.Println(err) @@ -161,7 +161,7 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent } for msgCounter, configs := range pendingDeviceConfigWrites { fmt.Printf("Approving LPC device config write with msgCounter %d for features: ", msgCounter) - for _, config := range(configs) { + for _, config := range configs { fmt.Printf("%s ", *config.Description.KeyName) } fmt.Print("\n") @@ -182,7 +182,7 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent // get pending writes pendingWrites := h.uccslpp.PendingProductionLimits() pendingDeviceConfigWrites := h.uccslpp.PendingDeviceConfigurations() - + // approve any write for msgCounter, write := range pendingWrites { fmt.Println("Approving LPP limit write with msgCounter", msgCounter, "and limit", write.Value, "W") @@ -190,7 +190,7 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent } for msgCounter, configs := range pendingDeviceConfigWrites { fmt.Printf("Approving LPP device config write with msgCounter %d for features: ", msgCounter) - for _, config := range(configs) { + for _, config := range configs { fmt.Printf("%s ", *config.Description.KeyName) } fmt.Print("\n") diff --git a/usecases/api/types.go b/usecases/api/types.go index cba0c7c0..cdde8255 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -168,7 +168,7 @@ type DurationSlotValue struct { } type PendingDeviceConfiguration struct { - Description *model.DeviceConfigurationKeyValueDescriptionDataType `json:"description,omitempty"` - Value *model.DeviceConfigurationKeyValueValueType `json:"value,omitempty"` - IsValueChangeable *bool `json:"isValueChangeable,omitempty" eebus:"writecheck"` -} \ No newline at end of file + Description *model.DeviceConfigurationKeyValueDescriptionDataType `json:"description,omitempty"` + Value *model.DeviceConfigurationKeyValueValueType `json:"value,omitempty"` + IsValueChangeable *bool `json:"isValueChangeable,omitempty" eebus:"writecheck"` +} diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go index e6fce4eb..a250b4de 100644 --- a/usecases/cs/lpc/public.go +++ b/usecases/cs/lpc/public.go @@ -273,7 +273,7 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool // return the currently pending incoming failsafe consumption limit writes func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) - + e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() @@ -289,10 +289,10 @@ func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.Pen if err != nil { continue } - + pendingConfigData := ucapi.PendingDeviceConfiguration{ - Description: description, - Value: configKeyValueData.Value, + Description: description, + Value: configKeyValueData.Value, IsValueChangeable: configKeyValueData.IsValueChangeable, } diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index 06f16b65..29536857 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -1,6 +1,7 @@ package lpc import ( + "slices" "sync" "github.com/enbility/eebus-go/api" @@ -22,8 +23,8 @@ type LPC struct { pendingMux sync.Mutex pendingLimits map[model.MsgCounterType]*spineapi.Message - pendingDeviceConfigMux sync.Mutex - pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message + pendingDeviceConfigMux sync.Mutex + pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message heartbeatDiag *features.DeviceDiagnosis @@ -80,8 +81,8 @@ func NewLPC(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa ) uc := &LPC{ - UseCaseBase: usecase, - pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + UseCaseBase: usecase, + pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), pendingDeviceConfigs: make(map[model.MsgCounterType]*spineapi.Message), } @@ -200,17 +201,25 @@ func (e *LPC) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bo // approves all others func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || - msg.Cmd.DeviceConfigurationKeyValueListData == nil { + msg.Cmd.DeviceConfigurationKeyValueListData == nil { logging.Log().Debug("LPC deviceConfigurationWriteCB: invalid message") return } data := msg.Cmd.DeviceConfigurationKeyValueListData - if len(data.DeviceConfigurationKeyValueData) == 0 || data.DeviceConfigurationKeyValueData[0].KeyId == nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: no data") - return - } + if data == nil || data.DeviceConfigurationKeyValueData == nil || len(data.DeviceConfigurationKeyValueData) == 0 { + logging.Log().Debug("LPC deviceConfigurationWriteCB: no data") + return + } + + // all DeviceConfigurationKeyValueData must have keyId set as primary identifier + if slices.ContainsFunc(data.DeviceConfigurationKeyValueData, func(i model.DeviceConfigurationKeyValueDataType) bool { + return i.KeyId == nil + }) { + logging.Log().Debug("LPC deviceConfigurationWriteCB: invalid message") + return + } dc, err := server.NewDeviceConfiguration(e.LocalEntity) if err != nil { @@ -219,7 +228,7 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: {}, - model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, + model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, } for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) @@ -238,12 +247,12 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { return } e.pendingDeviceConfigMux.Unlock() - } + } } // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept - e.approveOrDenyDeviceConfiguration(msg, true, "") -} + go e.approveOrDenyDeviceConfiguration(msg, true, "") +} func (e *LPC) AddFeatures() { // client features @@ -284,7 +293,7 @@ func (e *LPC) AddFeatures() { f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueDescriptionListData, true, false) f.AddFunctionType(model.FunctionTypeDeviceConfigurationKeyValueListData, true, true) _ = f.AddWriteApprovalCallback(e.deviceConfigurationWriteCB) - + if dcs, err := server.NewDeviceConfiguration(e.LocalEntity); err == nil { dcs.AddKeyValueDescription( model.DeviceConfigurationKeyValueDescriptionDataType{ diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go index 36a779f8..100a7f27 100644 --- a/usecases/cs/lpp/public.go +++ b/usecases/cs/lpp/public.go @@ -275,7 +275,7 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool // return the currently pending incoming failsafe consumption limit writes func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) - + e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() @@ -291,10 +291,10 @@ func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.Pen if err != nil { continue } - + pendingConfigData := ucapi.PendingDeviceConfiguration{ - Description: description, - Value: configKeyValueData.Value, + Description: description, + Value: configKeyValueData.Value, IsValueChangeable: configKeyValueData.IsValueChangeable, } diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index 62ca32e4..dec2c66c 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -1,6 +1,7 @@ package lpp import ( + "slices" "sync" "github.com/enbility/eebus-go/api" @@ -22,8 +23,8 @@ type LPP struct { pendingMux sync.Mutex pendingLimits map[model.MsgCounterType]*spineapi.Message - pendingDeviceConfigMux sync.Mutex - pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message + pendingDeviceConfigMux sync.Mutex + pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message heartbeatDiag *features.DeviceDiagnosis @@ -79,8 +80,8 @@ func NewLPP(localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCa ) uc := &LPP{ - UseCaseBase: usecase, - pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), + UseCaseBase: usecase, + pendingLimits: make(map[model.MsgCounterType]*spineapi.Message), pendingDeviceConfigs: make(map[model.MsgCounterType]*spineapi.Message), } @@ -200,28 +201,36 @@ func (e *LPP) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bo // approves all others func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || - msg.Cmd.DeviceConfigurationKeyValueListData == nil { + msg.Cmd.DeviceConfigurationKeyValueListData == nil { logging.Log().Debug("LPP deviceConfigurationWriteCB: invalid message") return } data := msg.Cmd.DeviceConfigurationKeyValueListData - if len(data.DeviceConfigurationKeyValueData) == 0 || data.DeviceConfigurationKeyValueData[0].KeyId == nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: no data") - return - } + if data == nil || data.DeviceConfigurationKeyValueData == nil || len(data.DeviceConfigurationKeyValueData) == 0 { + logging.Log().Debug("LPP deviceConfigurationWriteCB: no data") + return + } + + // all DeviceConfigurationKeyValueData must have keyId set as primary identifier + if slices.ContainsFunc(data.DeviceConfigurationKeyValueData, func(i model.DeviceConfigurationKeyValueDataType) bool { + return i.KeyId == nil + }) { + logging.Log().Debug("LPP deviceConfigurationWriteCB: invalid message") + return + } dc, err := server.NewDeviceConfiguration(e.LocalEntity) if err != nil { return } + configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: {}, - model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, + model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, } for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) if description == nil || err != nil { logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found") @@ -238,12 +247,12 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { return } e.pendingDeviceConfigMux.Unlock() - } + } } // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept - e.approveOrDenyDeviceConfiguration(msg, true, "") -} + go e.approveOrDenyDeviceConfiguration(msg, true, "") +} func (e *LPP) AddFeatures() { // client features From ea8625efde08104bd1deba2a831be94a43c39bd8 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Mon, 14 Jul 2025 17:42:47 +0200 Subject: [PATCH 07/11] Fix logging to include the KeyID for which no configuration exists --- usecases/cs/lpc/usecase.go | 2 +- usecases/cs/lpp/usecase.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index 29536857..eed274b2 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -233,7 +233,7 @@ func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) if description == nil || err != nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID %d found") + logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID found: ", *deviceKeyValueData.KeyId) continue } diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index dec2c66c..ceb28bf2 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -233,7 +233,7 @@ func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) if description == nil || err != nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID %d found") + logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID found: ", *deviceKeyValueData.KeyId) continue } From da2bc9724cd39331f01d08098f417b94a4acd8a6 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Mon, 14 Jul 2025 17:42:47 +0200 Subject: [PATCH 08/11] Update WriteApprovalRequired event documentation with new methods --- usecases/cs/lpc/types.go | 6 ++++-- usecases/cs/lpp/types.go | 14 ++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/usecases/cs/lpc/types.go b/usecases/cs/lpc/types.go index 0f828a73..5159c4b3 100644 --- a/usecases/cs/lpc/types.go +++ b/usecases/cs/lpc/types.go @@ -17,8 +17,10 @@ const ( // An incoming load control obligation limit needs to be approved or denied // - // Use `PendingConsumptionLimits` to get the currently pending write approval requests - // and invoke `ApproveOrDenyConsumptionLimit` for each + // Use `PendingConsumptionLimits` and `PendingDeviceConfigurations` to get + // the currently pending write approval requests and invoke + // `ApproveOrDenyConsumptionLimit` or `ApproveOrDenyDeviceConfiguration` + // for each // // Use Case LPC, Scenario 1 WriteApprovalRequired api.EventType = "cs-lpc-WriteApprovalRequired" diff --git a/usecases/cs/lpp/types.go b/usecases/cs/lpp/types.go index 0776d631..fb463e22 100644 --- a/usecases/cs/lpp/types.go +++ b/usecases/cs/lpp/types.go @@ -12,15 +12,17 @@ const ( // // Use `ProductionLimit` to get the current data // - // Use Case LPC, Scenario 1 + // Use Case LPP, Scenario 1 DataUpdateLimit api.EventType = "cs-lpp-DataUpdateLimit" // An incoming load control obligation limit needs to be approved or denied // - // Use `PendingProductionLimits` to get the currently pending write approval requests - // and invoke `ApproveOrDenyProductionLimit` for each + // Use `PendingProductionLimits` and `PendingDeviceConfigurations` to get + // the currently pending write approval requests and invoke + // `ApproveOrDenyProductionLimit` or `ApproveOrDenyDeviceConfiguration` for + // each // - // Use Case LPC, Scenario 1 + // Use Case LPP, Scenario 1 WriteApprovalRequired api.EventType = "cs-lpp-WriteApprovalRequired" // Failsafe limit for the produced active (real) power of the @@ -28,7 +30,7 @@ const ( // // Use `FailsafeProductionActivePowerLimit` to get the current data // - // Use Case LPC, Scenario 2 + // Use Case LPP, Scenario 2 DataUpdateFailsafeProductionActivePowerLimit api.EventType = "cs-lpp-DataUpdateFailsafeProductionActivePowerLimit" // Minimum time the Controllable System remains in "failsafe state" unless conditions @@ -36,7 +38,7 @@ const ( // // Use `FailsafeDurationMinimum` to get the current data // - // Use Case LPC, Scenario 2 + // Use Case LPP, Scenario 2 DataUpdateFailsafeDurationMinimum api.EventType = "cs-lpp-DataUpdateFailsafeDurationMinimum" // Indicates a notify heartbeat event the application should care of. From 57983227e95dde4cf0dc7263465c37d0198fbb2e Mon Sep 17 00:00:00 2001 From: Tom Luca Roth Date: Wed, 25 Feb 2026 10:06:35 +0100 Subject: [PATCH 09/11] Refactored device config write approval functionality that both LPC and LPP use into shared internal functions. Also added tests. --- examples/evse/main.go | 2 +- examples/hems/main.go | 28 +++- usecases/api/types.go | 2 +- usecases/cs/lpc/public.go | 31 +--- usecases/cs/lpc/public_test.go | 43 +++++ usecases/cs/lpc/types.go | 16 +- usecases/cs/lpc/usecase.go | 55 ++----- usecases/cs/lpc/usecase_test.go | 88 ++++++++++ usecases/cs/lpp/public.go | 33 +--- usecases/cs/lpp/public_test.go | 43 +++++ usecases/cs/lpp/types.go | 16 +- usecases/cs/lpp/usecase.go | 55 ++----- usecases/cs/lpp/usecase_test.go | 88 ++++++++++ usecases/internal/deviceconfiguration.go | 91 +++++++++++ usecases/internal/deviceconfiguration_test.go | 151 ++++++++++++++++++ 15 files changed, 584 insertions(+), 158 deletions(-) create mode 100644 usecases/internal/deviceconfiguration.go create mode 100644 usecases/internal/deviceconfiguration_test.go diff --git a/examples/evse/main.go b/examples/evse/main.go index 26256724..e7527db8 100644 --- a/examples/evse/main.go +++ b/examples/evse/main.go @@ -151,7 +151,7 @@ func (h *evse) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent } switch event { - case lpc.WriteApprovalRequired: + case lpc.LimitWriteApprovalRequired: // get pending writes pendingWrites := h.uclpc.PendingConsumptionLimits() diff --git a/examples/hems/main.go b/examples/hems/main.go index a4e16f1d..dc686e8a 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -149,20 +149,26 @@ func (h *hems) run() { func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { switch event { - case cslpc.WriteApprovalRequired: + case cslpc.LimitWriteApprovalRequired: // get pending writes pendingWrites := h.uccslpc.PendingConsumptionLimits() - pendingDeviceConfigWrites := h.uccslpc.PendingDeviceConfigurations() // approve any write for msgCounter, write := range pendingWrites { fmt.Println("Approving LPC limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpc.ApproveOrDenyConsumptionLimit(msgCounter, true, "") } + case cslpc.ConfigurationWriteApprovalRequired: + pendingDeviceConfigWrites := h.uccslpc.PendingDeviceConfigurations() + for msgCounter, configs := range pendingDeviceConfigWrites { fmt.Printf("Approving LPC device config write with msgCounter %d for features: ", msgCounter) for _, config := range configs { - fmt.Printf("%s ", *config.Description.KeyName) + keyName := "nil" + if config.Description.KeyName != nil { + keyName = string(*config.Description.KeyName) + } + fmt.Printf("%s ", keyName) } fmt.Print("\n") h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") @@ -178,20 +184,28 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { switch event { - case cslpp.WriteApprovalRequired: - // get pending writes + case cslpp.LimitWriteApprovalRequired: + // get pending limit writes pendingWrites := h.uccslpp.PendingProductionLimits() - pendingDeviceConfigWrites := h.uccslpp.PendingDeviceConfigurations() // approve any write for msgCounter, write := range pendingWrites { fmt.Println("Approving LPP limit write with msgCounter", msgCounter, "and limit", write.Value, "W") h.uccslpp.ApproveOrDenyProductionLimit(msgCounter, true, "") } + case cslpp.ConfigurationWriteApprovalRequired: + // get pending device config writes + pendingDeviceConfigWrites := h.uccslpp.PendingDeviceConfigurations() + + // approve any write for msgCounter, configs := range pendingDeviceConfigWrites { fmt.Printf("Approving LPP device config write with msgCounter %d for features: ", msgCounter) for _, config := range configs { - fmt.Printf("%s ", *config.Description.KeyName) + keyName := "nil" + if config.Description.KeyName != nil { + keyName = string(*config.Description.KeyName) + } + fmt.Printf("%s ", keyName) } fmt.Print("\n") h.uccslpp.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") diff --git a/usecases/api/types.go b/usecases/api/types.go index cdde8255..e26a7991 100644 --- a/usecases/api/types.go +++ b/usecases/api/types.go @@ -170,5 +170,5 @@ type DurationSlotValue struct { type PendingDeviceConfiguration struct { Description *model.DeviceConfigurationKeyValueDescriptionDataType `json:"description,omitempty"` Value *model.DeviceConfigurationKeyValueValueType `json:"value,omitempty"` - IsValueChangeable *bool `json:"isValueChangeable,omitempty" eebus:"writecheck"` + IsValueChangeable *bool `json:"isValueChangeable,omitempty"` } diff --git a/usecases/cs/lpc/public.go b/usecases/cs/lpc/public.go index a250b4de..566108de 100644 --- a/usecases/cs/lpc/public.go +++ b/usecases/cs/lpc/public.go @@ -7,6 +7,7 @@ import ( "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/features/server" ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/internal" "github.com/enbility/spine-go/model" "github.com/enbility/spine-go/util" ) @@ -272,38 +273,10 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool // return the currently pending incoming failsafe consumption limit writes func (e *LPC) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { - result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) - e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() - dc, err := server.NewDeviceConfiguration(e.LocalEntity) - if err != nil { - return result - } - - for msgCounter, msg := range e.pendingDeviceConfigs { - data := msg.Cmd.DeviceConfigurationKeyValueListData - for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dc.GetKeyValueDescriptionFoKeyId(*configKeyValueData.KeyId) - if err != nil { - continue - } - - pendingConfigData := ucapi.PendingDeviceConfiguration{ - Description: description, - Value: configKeyValueData.Value, - IsValueChangeable: configKeyValueData.IsValueChangeable, - } - - if _, exists := result[msgCounter]; !exists { - result[msgCounter] = []ucapi.PendingDeviceConfiguration{pendingConfigData} - } else { - result[msgCounter] = append(result[msgCounter], pendingConfigData) - } - } - } - return result + return internal.GroupPendingDeviceConfigurations(e.pendingDeviceConfigs, e.LocalEntity) } // accept or deny an incoming device configuration write diff --git a/usecases/cs/lpc/public_test.go b/usecases/cs/lpc/public_test.go index 152f4890..f095c9ee 100644 --- a/usecases/cs/lpc/public_test.go +++ b/usecases/cs/lpc/public_test.go @@ -170,3 +170,46 @@ func (s *CsLPCSuite) Test_ConsumptionNominalMax() { assert.Equal(s.T(), 10.0, value) assert.Nil(s.T(), err) } + +func (s *CsLPCSuite) Test_PendingDeviceConfigurations() { + data := s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 0, len(data)) + + msgCounter := model.MsgCounterType(500) + + msg := &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(msgCounter), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(1000), + }, + IsValueChangeable: util.Ptr(true), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.deviceConfigurationWriteCB(msg) + + data = s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyDeviceConfiguration(model.MsgCounterType(499), true, "") + + data = s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyDeviceConfiguration(msgCounter, false, "leave me alone") + + data = s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 0, len(data)) +} diff --git a/usecases/cs/lpc/types.go b/usecases/cs/lpc/types.go index 5159c4b3..640f982a 100644 --- a/usecases/cs/lpc/types.go +++ b/usecases/cs/lpc/types.go @@ -17,13 +17,19 @@ const ( // An incoming load control obligation limit needs to be approved or denied // - // Use `PendingConsumptionLimits` and `PendingDeviceConfigurations` to get - // the currently pending write approval requests and invoke - // `ApproveOrDenyConsumptionLimit` or `ApproveOrDenyDeviceConfiguration` - // for each + // Use `PendingConsumptionLimits` to get the currently pending limit write + // approval requests and invoke `ApproveOrDenyConsumptionLimit` for each // // Use Case LPC, Scenario 1 - WriteApprovalRequired api.EventType = "cs-lpc-WriteApprovalRequired" + LimitWriteApprovalRequired api.EventType = "cs-lpc-LimitWriteApprovalRequired" + + // An incoming device configuration write needs to be approved or denied + // + // Use `PendingDeviceConfigurations` to get the currently pending device configuration + // write approval requests and invoke `ApproveOrDenyDeviceConfiguration` for each + // + // Use Case LPC, Scenario 1 + ConfigurationWriteApprovalRequired api.EventType = "cs-lpc-ConfigurationWriteApprovalRequired" // Failsafe limit for the consumed active (real) power of the // Controllable System data update received diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index eed274b2..a0a5c74b 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -1,7 +1,6 @@ package lpc import ( - "slices" "sync" "github.com/enbility/eebus-go/api" @@ -170,7 +169,7 @@ func (e *LPC) loadControlWriteCB(msg *spineapi.Message) { if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok { e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg e.pendingMux.Unlock() - e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, LimitWriteApprovalRequired) return } } @@ -200,57 +199,33 @@ func (e *LPC) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bo // the implementation only considers write messages for this use case and // approves all others func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { - if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || - msg.Cmd.DeviceConfigurationKeyValueListData == nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: invalid message") - return - } - - data := msg.Cmd.DeviceConfigurationKeyValueListData - - if data == nil || data.DeviceConfigurationKeyValueData == nil || len(data.DeviceConfigurationKeyValueData) == 0 { - logging.Log().Debug("LPC deviceConfigurationWriteCB: no data") - return - } - - // all DeviceConfigurationKeyValueData must have keyId set as primary identifier - if slices.ContainsFunc(data.DeviceConfigurationKeyValueData, func(i model.DeviceConfigurationKeyValueDataType) bool { - return i.KeyId == nil - }) { + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil { logging.Log().Debug("LPC deviceConfigurationWriteCB: invalid message") return } - dc, err := server.NewDeviceConfiguration(e.LocalEntity) - if err != nil { - return - } - configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: {}, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, } - for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) - if description == nil || err != nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: no device configuration for KeyID found: ", *deviceKeyValueData.KeyId) - continue - } + approvalRequired, err := internal.ConfigurationWriteRequiresApproval(msg, e.LocalEntity, configsToApprove) + if err != nil { + logging.Log().Errorf("LPC deviceConfigurationWriteCB: %s", err.Error()) + return + } - // Only ask for write approval if at least one of the configurations we care about is trying to be set - if _, exists := configsToApprove[*description.KeyName]; exists { - e.pendingDeviceConfigMux.Lock() - if _, exists := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !exists { - e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = msg - e.pendingDeviceConfigMux.Unlock() - e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) - return - } + if approvalRequired { + e.pendingDeviceConfigMux.Lock() + if _, exists := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !exists { + e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = msg e.pendingDeviceConfigMux.Unlock() + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, ConfigurationWriteApprovalRequired) + return } + e.pendingDeviceConfigMux.Unlock() } - // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this use case so we accept go e.approveOrDenyDeviceConfiguration(msg, true, "") } diff --git a/usecases/cs/lpc/usecase_test.go b/usecases/cs/lpc/usecase_test.go index 406280f9..1bd6ac3c 100644 --- a/usecases/cs/lpc/usecase_test.go +++ b/usecases/cs/lpc/usecase_test.go @@ -135,6 +135,94 @@ func (s *CsLPCSuite) Test_loadControlWriteCB() { s.eventCalled = false } +func (s *CsLPCSuite) Test_deviceConfigurationWriteCB() { + // Header missing + msg := &spineapi.Message{} + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // MsgCounter missing + msg = &spineapi.Message{RequestHeader: &model.HeaderType{}} + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // Message is invalid (DeviceConfigurationKeyValueData missing) + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(600)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{}, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // Unrelated KeyId + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(603)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // Failsafe consumption active power limit write -> approval required. + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(603)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.True(s.T(), s.eventCalled) + s.eventCalled = false + + // Failsafe duration minimum write -> approval required. + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(604)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.True(s.T(), s.eventCalled) + s.eventCalled = false +} + func (s *CsLPCSuite) Test_UpdateUseCaseAvailability() { s.sut.UpdateUseCaseAvailability(true) } diff --git a/usecases/cs/lpp/public.go b/usecases/cs/lpp/public.go index 100a7f27..72ff19e8 100644 --- a/usecases/cs/lpp/public.go +++ b/usecases/cs/lpp/public.go @@ -7,6 +7,7 @@ import ( "github.com/enbility/eebus-go/api" "github.com/enbility/eebus-go/features/server" ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/eebus-go/usecases/internal" "github.com/enbility/spine-go/model" "github.com/enbility/spine-go/util" ) @@ -272,40 +273,12 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool return dc.UpdateKeyValueDataForFilter(data, nil, filter) } -// return the currently pending incoming failsafe consumption limit writes +// return the currently pending incoming failsafe production limit writes func (e *LPP) PendingDeviceConfigurations() map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { - result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) - e.pendingDeviceConfigMux.Lock() defer e.pendingDeviceConfigMux.Unlock() - dc, err := server.NewDeviceConfiguration(e.LocalEntity) - if err != nil { - return result - } - - for msgCounter, msg := range e.pendingDeviceConfigs { - data := msg.Cmd.DeviceConfigurationKeyValueListData - for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dc.GetKeyValueDescriptionFoKeyId(*configKeyValueData.KeyId) - if err != nil { - continue - } - - pendingConfigData := ucapi.PendingDeviceConfiguration{ - Description: description, - Value: configKeyValueData.Value, - IsValueChangeable: configKeyValueData.IsValueChangeable, - } - - if _, exists := result[msgCounter]; !exists { - result[msgCounter] = []ucapi.PendingDeviceConfiguration{pendingConfigData} - } else { - result[msgCounter] = append(result[msgCounter], pendingConfigData) - } - } - } - return result + return internal.GroupPendingDeviceConfigurations(e.pendingDeviceConfigs, e.LocalEntity) } // accept or deny an incoming device configuration write diff --git a/usecases/cs/lpp/public_test.go b/usecases/cs/lpp/public_test.go index e768228e..5cad26de 100644 --- a/usecases/cs/lpp/public_test.go +++ b/usecases/cs/lpp/public_test.go @@ -169,3 +169,46 @@ func (s *CsLPPSuite) Test_ContractualProductionNominalMax() { assert.Equal(s.T(), 10.0, value) assert.Nil(s.T(), err) } + +func (s *CsLPPSuite) Test_PendingDeviceConfigurations() { + data := s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 0, len(data)) + + msgCounter := model.MsgCounterType(500) + + msg := &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(msgCounter), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + Value: &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(1000), + }, + IsValueChangeable: util.Ptr(true), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + + s.sut.deviceConfigurationWriteCB(msg) + + data = s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyDeviceConfiguration(model.MsgCounterType(499), true, "") + + data = s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 1, len(data)) + + s.sut.ApproveOrDenyDeviceConfiguration(msgCounter, false, "leave me alone") + + data = s.sut.PendingDeviceConfigurations() + assert.Equal(s.T(), 0, len(data)) +} diff --git a/usecases/cs/lpp/types.go b/usecases/cs/lpp/types.go index fb463e22..624aec55 100644 --- a/usecases/cs/lpp/types.go +++ b/usecases/cs/lpp/types.go @@ -17,13 +17,19 @@ const ( // An incoming load control obligation limit needs to be approved or denied // - // Use `PendingProductionLimits` and `PendingDeviceConfigurations` to get - // the currently pending write approval requests and invoke - // `ApproveOrDenyProductionLimit` or `ApproveOrDenyDeviceConfiguration` for - // each + // Use `PendingProductionLimits` to get the currently pending limit write + // approval requests and invoke `ApproveOrDenyProductionLimit` for each // // Use Case LPP, Scenario 1 - WriteApprovalRequired api.EventType = "cs-lpp-WriteApprovalRequired" + LimitWriteApprovalRequired api.EventType = "cs-lpp-LimitWriteApprovalRequired" + + // An incoming device configuration write needs to be approved or denied + // + // Use `PendingDeviceConfigurations` to get the currently pending device configuration + // write approval requests and invoke `ApproveOrDenyDeviceConfiguration` for each + // + // Use Case LPP, Scenario 1 + ConfigurationWriteApprovalRequired api.EventType = "cs-lpp-ConfigurationWriteApprovalRequired" // Failsafe limit for the produced active (real) power of the // Controllable System data update received diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index ceb28bf2..920ef5ba 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -1,7 +1,6 @@ package lpp import ( - "slices" "sync" "github.com/enbility/eebus-go/api" @@ -169,7 +168,7 @@ func (e *LPP) loadControlWriteCB(msg *spineapi.Message) { if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok { e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg e.pendingMux.Unlock() - e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, LimitWriteApprovalRequired) return } } @@ -200,57 +199,33 @@ func (e *LPP) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bo // the implementation only considers write messages for this use case and // approves all others func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { - if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || - msg.Cmd.DeviceConfigurationKeyValueListData == nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: invalid message") - return - } - - data := msg.Cmd.DeviceConfigurationKeyValueListData - - if data == nil || data.DeviceConfigurationKeyValueData == nil || len(data.DeviceConfigurationKeyValueData) == 0 { - logging.Log().Debug("LPP deviceConfigurationWriteCB: no data") - return - } - - // all DeviceConfigurationKeyValueData must have keyId set as primary identifier - if slices.ContainsFunc(data.DeviceConfigurationKeyValueData, func(i model.DeviceConfigurationKeyValueDataType) bool { - return i.KeyId == nil - }) { + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil { logging.Log().Debug("LPP deviceConfigurationWriteCB: invalid message") return } - dc, err := server.NewDeviceConfiguration(e.LocalEntity) - if err != nil { - return - } - configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: {}, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, } - for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { - description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) - if description == nil || err != nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: no device configuration for KeyID found: ", *deviceKeyValueData.KeyId) - continue - } + approvalRequired, err := internal.ConfigurationWriteRequiresApproval(msg, e.LocalEntity, configsToApprove) + if err != nil { + logging.Log().Errorf("LPP deviceConfigurationWriteCB: %s", err.Error()) + return + } - // Only ask for write approval if at least one of the configurations we care about is trying to be set - if _, exists := configsToApprove[*description.KeyName]; exists { - e.pendingDeviceConfigMux.Lock() - if _, exists := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !exists { - e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = msg - e.pendingDeviceConfigMux.Unlock() - e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired) - return - } + if approvalRequired { + e.pendingDeviceConfigMux.Lock() + if _, exists := e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter]; !exists { + e.pendingDeviceConfigs[*msg.RequestHeader.MsgCounter] = msg e.pendingDeviceConfigMux.Unlock() + e.EventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, ConfigurationWriteApprovalRequired) + return } + e.pendingDeviceConfigMux.Unlock() } - // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept + // If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this use case so we accept go e.approveOrDenyDeviceConfiguration(msg, true, "") } diff --git a/usecases/cs/lpp/usecase_test.go b/usecases/cs/lpp/usecase_test.go index baaaf1c1..35bfcb55 100644 --- a/usecases/cs/lpp/usecase_test.go +++ b/usecases/cs/lpp/usecase_test.go @@ -78,6 +78,94 @@ func (s *CsLPPSuite) Test_loadControlWriteCB() { s.sut.loadControlWriteCB(msg) } +func (s *CsLPPSuite) Test_deviceConfigurationWriteCB() { + // Header missing + msg := &spineapi.Message{} + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // MsgCounter missing + msg = &spineapi.Message{RequestHeader: &model.HeaderType{}} + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // Message is invalid (DeviceConfigurationKeyValueData missing) + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(600)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{}, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // Unrelated KeyId + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(603)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2)), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.False(s.T(), s.eventCalled) + + // Failsafe production active power limit write -> approval required. + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(603)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.True(s.T(), s.eventCalled) + s.eventCalled = false + + // Failsafe duration minimum write -> approval required. + msg = &spineapi.Message{ + RequestHeader: &model.HeaderType{ + MsgCounter: util.Ptr(model.MsgCounterType(604)), + }, + Cmd: model.CmdType{ + DeviceConfigurationKeyValueListData: &model.DeviceConfigurationKeyValueListDataType{ + DeviceConfigurationKeyValueData: []model.DeviceConfigurationKeyValueDataType{ + { + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + }, + }, + }, + }, + DeviceRemote: s.remoteDevice, + EntityRemote: s.monitoredEntity, + } + s.sut.deviceConfigurationWriteCB(msg) + assert.True(s.T(), s.eventCalled) + s.eventCalled = false +} + func (s *CsLPPSuite) Test_UpdateUseCaseAvailability() { s.sut.UpdateUseCaseAvailability(true) } diff --git a/usecases/internal/deviceconfiguration.go b/usecases/internal/deviceconfiguration.go new file mode 100644 index 00000000..78432bf3 --- /dev/null +++ b/usecases/internal/deviceconfiguration.go @@ -0,0 +1,91 @@ +package internal + +import ( + "fmt" + "slices" + + "github.com/enbility/eebus-go/features/server" + ucapi "github.com/enbility/eebus-go/usecases/api" + "github.com/enbility/ship-go/logging" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" +) + +// Check if an incoming device configuration write requires write approval, if yes return `true` otherwise `false` +func ConfigurationWriteRequiresApproval(msg *spineapi.Message, localEntity spineapi.EntityLocalInterface, configsToApprove map[model.DeviceConfigurationKeyNameType]struct{}) (bool, error) { + if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil || + msg.Cmd.DeviceConfigurationKeyValueListData == nil { + return false, fmt.Errorf("invalid message") + } + + data := msg.Cmd.DeviceConfigurationKeyValueListData + + if len(data.DeviceConfigurationKeyValueData) == 0 { + return false, fmt.Errorf("no data") + } + + // all DeviceConfigurationKeyValueData must have keyId set as primary identifier + if slices.ContainsFunc(data.DeviceConfigurationKeyValueData, func(i model.DeviceConfigurationKeyValueDataType) bool { + return i.KeyId == nil + }) { + return false, fmt.Errorf("invalid message") + } + + dc, err := server.NewDeviceConfiguration(localEntity) + if err != nil { + return false, err + } + + for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData { + description, err := dc.GetKeyValueDescriptionFoKeyId(*deviceKeyValueData.KeyId) + if description == nil || err != nil { + logging.Log().Debug("ConfigurationWriteRequiresApproval: no device configuration for KeyID found: ", *deviceKeyValueData.KeyId) + continue + } + + if description.KeyName == nil { + logging.Log().Debugf("ConfigurationWriteRequiresApproval: invalid internal data (KeyName not set on DeviceConfigurationKeyValueDescriptionDataType with key %s)", *deviceKeyValueData.KeyId) + continue + } + + // Only ask for write approval if at least one of the configurations we care about is trying to be set + if _, exists := configsToApprove[*description.KeyName]; exists { + return true, nil + } + } + return false, nil +} + +// Extract the device configuration writes from each pending message and return them grouped in a map by msgCounter +func GroupPendingDeviceConfigurations(pendingDeviceConfigs map[model.MsgCounterType]*spineapi.Message, localEntity spineapi.EntityLocalInterface) map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration { + result := make(map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration) + + dc, err := server.NewDeviceConfiguration(localEntity) + if err != nil { + logging.Log().Debugf("AggregatePendingDeviceConfigurations: Error occurred when getting device configuration: %s", err.Error()) + return result + } + + for msgCounter, msg := range pendingDeviceConfigs { + data := msg.Cmd.DeviceConfigurationKeyValueListData + for _, configKeyValueData := range data.DeviceConfigurationKeyValueData { + description, err := dc.GetKeyValueDescriptionFoKeyId(*configKeyValueData.KeyId) + if err != nil { + continue + } + + pendingConfigData := ucapi.PendingDeviceConfiguration{ + Description: description, + Value: configKeyValueData.Value, + IsValueChangeable: configKeyValueData.IsValueChangeable, + } + + if _, exists := result[msgCounter]; !exists { + result[msgCounter] = []ucapi.PendingDeviceConfiguration{pendingConfigData} + } else { + result[msgCounter] = append(result[msgCounter], pendingConfigData) + } + } + } + return result +} diff --git a/usecases/internal/deviceconfiguration_test.go b/usecases/internal/deviceconfiguration_test.go new file mode 100644 index 00000000..9ae55309 --- /dev/null +++ b/usecases/internal/deviceconfiguration_test.go @@ -0,0 +1,151 @@ +package internal + +import ( + "github.com/enbility/eebus-go/features/server" + ucapi "github.com/enbility/eebus-go/usecases/api" + spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + + "github.com/stretchr/testify/assert" +) + +func (s *InternalSuite) Test_ConfigurationWriteRequiresApproval() { + msg := spineapi.Message{} + configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ + model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: {}, + model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, + } + // Header missing + _, err := ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.NotNil(s.T(), err) + + // MsgCounter missing + header := model.HeaderType{} + msg = spineapi.Message{RequestHeader: &header} + _, err = ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.NotNil(s.T(), err) + + // DeviceConfigurationKeyValueListData missing + header = model.HeaderType{MsgCounter: util.Ptr(model.MsgCounterType(1))} + msg = spineapi.Message{RequestHeader: &header} + _, err = ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.NotNil(s.T(), err) + + // DeviceConfigurationKeyValueListData.DeviceConfigurationKeyValueData is nil/length of 0 + cmd := model.CmdType{DeviceConfigurationKeyValueListData: util.Ptr(model.DeviceConfigurationKeyValueListDataType{})} + msg = spineapi.Message{RequestHeader: &header, Cmd: cmd} + _, err = ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.NotNil(s.T(), err) + + // Not all elements in slice of DeviceConfigurationKeyValueDataType have KeyId set + deviceConfigList := []model.DeviceConfigurationKeyValueDataType{{KeyId: nil}} + cmd = model.CmdType{DeviceConfigurationKeyValueListData: util.Ptr(model.DeviceConfigurationKeyValueListDataType{DeviceConfigurationKeyValueData: deviceConfigList})} + msg = spineapi.Message{RequestHeader: &header, Cmd: cmd} + _, err = ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.NotNil(s.T(), err) + + // Valid message but not a KeyId we care about => no approval required + deviceConfigList = []model.DeviceConfigurationKeyValueDataType{{KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0))}} + cmd = model.CmdType{DeviceConfigurationKeyValueListData: util.Ptr(model.DeviceConfigurationKeyValueListDataType{DeviceConfigurationKeyValueData: deviceConfigList})} + msg = spineapi.Message{RequestHeader: &header, Cmd: cmd} + approvalRequired, err := ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.Nil(s.T(), err) + assert.False(s.T(), approvalRequired) + + // Valid message with KeyId we care about => approval required + if dcs, err := server.NewDeviceConfiguration(s.localEntity); err == nil { + dcs.AddKeyValueDescription( + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + }, + ) + + value := &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(true), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + ) + } + approvalRequired, err = ConfigurationWriteRequiresApproval(&msg, s.localEntity, configsToApprove) + assert.Nil(s.T(), err) + assert.True(s.T(), approvalRequired) +} + +func (s *InternalSuite) Test_GroupPendingDeviceConfigurations() { + failsafeLimitDesc := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeScaledNumber), + Unit: util.Ptr(model.UnitOfMeasurementTypeW), + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0)), + } + failsafeDurationMinDesc := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + ValueType: util.Ptr(model.DeviceConfigurationKeyValueTypeTypeDuration), + KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1)), + } + if dcs, err := server.NewDeviceConfiguration(s.localEntity); err == nil { + dcs.AddKeyValueDescription(failsafeLimitDesc) + + // only add if it doesn't exist yet + filter := model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + } + if data, err := dcs.GetKeyValueDescriptionsForFilter(filter); err == nil && len(data) == 0 { + dcs.AddKeyValueDescription(failsafeDurationMinDesc) + } + + value := &model.DeviceConfigurationKeyValueValueType{ + ScaledNumber: model.NewScaledNumberType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(true), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit), + }, + ) + + value = &model.DeviceConfigurationKeyValueValueType{ + Duration: model.NewDurationType(0), + } + _ = dcs.UpdateKeyValueDataForFilter( + model.DeviceConfigurationKeyValueDataType{ + Value: value, + IsValueChangeable: util.Ptr(true), + }, + nil, + model.DeviceConfigurationKeyValueDescriptionDataType{ + KeyName: util.Ptr(model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum), + }, + ) + } + + deviceConfigList := []model.DeviceConfigurationKeyValueDataType{{KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(0))}, {KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(1))}, {KeyId: util.Ptr(model.DeviceConfigurationKeyIdType(2))}} + cmd := model.CmdType{DeviceConfigurationKeyValueListData: util.Ptr(model.DeviceConfigurationKeyValueListDataType{DeviceConfigurationKeyValueData: deviceConfigList})} + msg := spineapi.Message{Cmd: cmd} + pendingDeviceConfigs := map[model.MsgCounterType]*spineapi.Message{model.MsgCounterType(1): &msg} + groupedConfigurations := GroupPendingDeviceConfigurations(pendingDeviceConfigs, s.localEntity) + // For one of the KeyIds no corresponding device configuration exists, that element should thus be skipped + expected := map[model.MsgCounterType][]ucapi.PendingDeviceConfiguration{ + model.MsgCounterType(1): { + {Description: &failsafeLimitDesc}, + {Description: &failsafeDurationMinDesc}, + }, + } + assert.Equal(s.T(), groupedConfigurations, expected) + +} From af9d19184f1f301d50d8cedb86bd02968835491d Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Wed, 25 Feb 2026 10:39:27 +0100 Subject: [PATCH 10/11] Clarify comments describing events --- usecases/cs/lpc/types.go | 8 ++++---- usecases/cs/lpp/types.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/usecases/cs/lpc/types.go b/usecases/cs/lpc/types.go index 640f982a..03974a7d 100644 --- a/usecases/cs/lpc/types.go +++ b/usecases/cs/lpc/types.go @@ -17,16 +17,16 @@ const ( // An incoming load control obligation limit needs to be approved or denied // - // Use `PendingConsumptionLimits` to get the currently pending limit write - // approval requests and invoke `ApproveOrDenyConsumptionLimit` for each + // Use `PendingConsumptionLimits` to get the currently pending consumption limits + // awaiting approval and invoke `ApproveOrDenyConsumptionLimit` for each // // Use Case LPC, Scenario 1 LimitWriteApprovalRequired api.EventType = "cs-lpc-LimitWriteApprovalRequired" // An incoming device configuration write needs to be approved or denied // - // Use `PendingDeviceConfigurations` to get the currently pending device configuration - // write approval requests and invoke `ApproveOrDenyDeviceConfiguration` for each + // Use `PendingDeviceConfigurations` to get the currently pending device configurations + // awaiting approval and invoke `ApproveOrDenyDeviceConfiguration` for each // // Use Case LPC, Scenario 1 ConfigurationWriteApprovalRequired api.EventType = "cs-lpc-ConfigurationWriteApprovalRequired" diff --git a/usecases/cs/lpp/types.go b/usecases/cs/lpp/types.go index 624aec55..0f5f36e8 100644 --- a/usecases/cs/lpp/types.go +++ b/usecases/cs/lpp/types.go @@ -17,16 +17,16 @@ const ( // An incoming load control obligation limit needs to be approved or denied // - // Use `PendingProductionLimits` to get the currently pending limit write - // approval requests and invoke `ApproveOrDenyProductionLimit` for each + // Use `PendingProductionLimits` to get the currently pending produciton limits + // awaiting approval and invoke `ApproveOrDenyProductionLimit` for each // // Use Case LPP, Scenario 1 LimitWriteApprovalRequired api.EventType = "cs-lpp-LimitWriteApprovalRequired" // An incoming device configuration write needs to be approved or denied // - // Use `PendingDeviceConfigurations` to get the currently pending device configuration - // write approval requests and invoke `ApproveOrDenyDeviceConfiguration` for each + // Use `PendingDeviceConfigurations` to get the currently pending device configurations + // awaiting approval and invoke `ApproveOrDenyDeviceConfiguration` for each // // Use Case LPP, Scenario 1 ConfigurationWriteApprovalRequired api.EventType = "cs-lpp-ConfigurationWriteApprovalRequired" From c04b7fccf1e7236a20e117485aebe522ce20f4d6 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Wed, 25 Feb 2026 10:59:36 +0100 Subject: [PATCH 11/11] Simplify and clean up logging --- examples/hems/main.go | 8 ++------ usecases/cs/lpc/usecase.go | 5 ----- usecases/cs/lpp/usecase.go | 5 ----- usecases/internal/deviceconfiguration.go | 2 +- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/examples/hems/main.go b/examples/hems/main.go index dc686e8a..70356ea2 100644 --- a/examples/hems/main.go +++ b/examples/hems/main.go @@ -164,11 +164,9 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent for msgCounter, configs := range pendingDeviceConfigWrites { fmt.Printf("Approving LPC device config write with msgCounter %d for features: ", msgCounter) for _, config := range configs { - keyName := "nil" if config.Description.KeyName != nil { - keyName = string(*config.Description.KeyName) + fmt.Printf("%s ", string(*config.Description.KeyName)) } - fmt.Printf("%s ", keyName) } fmt.Print("\n") h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") @@ -201,11 +199,9 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent for msgCounter, configs := range pendingDeviceConfigWrites { fmt.Printf("Approving LPP device config write with msgCounter %d for features: ", msgCounter) for _, config := range configs { - keyName := "nil" if config.Description.KeyName != nil { - keyName = string(*config.Description.KeyName) + fmt.Printf("%s ", string(*config.Description.KeyName)) } - fmt.Printf("%s ", keyName) } fmt.Print("\n") h.uccslpp.ApproveOrDenyDeviceConfiguration(msgCounter, true, "") diff --git a/usecases/cs/lpc/usecase.go b/usecases/cs/lpc/usecase.go index a0a5c74b..e36d3aa3 100644 --- a/usecases/cs/lpc/usecase.go +++ b/usecases/cs/lpc/usecase.go @@ -199,11 +199,6 @@ func (e *LPC) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bo // the implementation only considers write messages for this use case and // approves all others func (e *LPC) deviceConfigurationWriteCB(msg *spineapi.Message) { - if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil { - logging.Log().Debug("LPC deviceConfigurationWriteCB: invalid message") - return - } - configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: {}, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, diff --git a/usecases/cs/lpp/usecase.go b/usecases/cs/lpp/usecase.go index 920ef5ba..4224ccc4 100644 --- a/usecases/cs/lpp/usecase.go +++ b/usecases/cs/lpp/usecase.go @@ -199,11 +199,6 @@ func (e *LPP) approveOrDenyDeviceConfiguration(msg *spineapi.Message, approve bo // the implementation only considers write messages for this use case and // approves all others func (e *LPP) deviceConfigurationWriteCB(msg *spineapi.Message) { - if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil { - logging.Log().Debug("LPP deviceConfigurationWriteCB: invalid message") - return - } - configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{ model.DeviceConfigurationKeyNameTypeFailsafeProductionActivePowerLimit: {}, model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {}, diff --git a/usecases/internal/deviceconfiguration.go b/usecases/internal/deviceconfiguration.go index 78432bf3..a2267029 100644 --- a/usecases/internal/deviceconfiguration.go +++ b/usecases/internal/deviceconfiguration.go @@ -62,7 +62,7 @@ func GroupPendingDeviceConfigurations(pendingDeviceConfigs map[model.MsgCounterT dc, err := server.NewDeviceConfiguration(localEntity) if err != nil { - logging.Log().Debugf("AggregatePendingDeviceConfigurations: Error occurred when getting device configuration: %s", err.Error()) + logging.Log().Debugf("GroupPendingDeviceConfigurations: Error occurred when getting device configuration: %s", err.Error()) return result }