Skip to content
22 changes: 20 additions & 2 deletions examples/hems/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,21 @@ func (h *hems) OnLPCEvent(ski string, device spineapi.DeviceRemoteInterface, ent
case cslpc.WriteApprovalRequired:
// get pending writes
pendingWrites := h.uccslpc.PendingConsumptionLimits()
pendingDeviceConfigWrites := h.uccslpc.PendingDeviceConfigurations()

// 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, 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)
}
fmt.Print("\n")
h.uccslpc.ApproveOrDenyDeviceConfiguration(msgCounter, true, "")
}
case cslpc.DataUpdateLimit:
if currentLimit, err := h.uccslpc.ConsumptionLimit(); err == nil {
fmt.Println("New LPC Limit set to", currentLimit.Value, "W")
Expand All @@ -172,12 +181,21 @@ func (h *hems) OnLPPEvent(ski string, device spineapi.DeviceRemoteInterface, ent
case cslpp.WriteApprovalRequired:
// get pending writes
pendingWrites := h.uccslpp.PendingProductionLimits()
pendingDeviceConfigWrites := h.uccslpp.PendingDeviceConfigurations()

// 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, 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)
}
fmt.Print("\n")
h.uccslpp.ApproveOrDenyDeviceConfiguration(msgCounter, true, "")
}
case cslpp.DataUpdateLimit:
if currentLimit, err := h.uccslpp.ProductionLimit(); err == nil {
fmt.Println("New LPP Limit set to", currentLimit.Value, "W")
Expand Down
11 changes: 11 additions & 0 deletions usecases/api/cs_lpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 device configuration writes
PendingDeviceConfigurations() map[model.MsgCounterType][]PendingDeviceConfiguration

// 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
ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, approve bool, reason string)

// Scenario 3

// start sending heartbeat from the local entity supporting this usecase
Expand Down
11 changes: 11 additions & 0 deletions usecases/api/cs_lpp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 device configuration writes
PendingDeviceConfigurations() map[model.MsgCounterType][]PendingDeviceConfiguration

// 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
ApproveOrDenyDeviceConfiguration(msgCounter model.MsgCounterType, approve bool, reason string)

// Scenario 3

// start sending heartbeat from the local entity supporting this usecase
Expand Down
6 changes: 6 additions & 0 deletions usecases/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,9 @@ 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"`
}
53 changes: 53 additions & 0 deletions usecases/cs/lpc/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,59 @@ func (e *LPC) SetFailsafeDurationMinimum(duration time.Duration, changeable bool
return dc.UpdateKeyValueDataForFilter(data, nil, filter)
}

// 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
}

// accept or deny an incoming device configuration write
//
// 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.pendingDeviceConfigs[msgCounter]
if !ok {
// no pending limit for this msgCounter, this is a caller error
return
}

e.approveOrDenyDeviceConfiguration(msg, approve, reason)
delete(e.pendingDeviceConfigs, msgCounter)
}

// Scenario 3

// start sending heartbeat from the local entity supporting this usecase
Expand Down
6 changes: 4 additions & 2 deletions usecases/cs/lpc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
84 changes: 82 additions & 2 deletions usecases/cs/lpc/usecase.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lpc

import (
"slices"
"sync"

"github.com/enbility/eebus-go/api"
Expand All @@ -21,6 +22,9 @@ type LPC struct {
pendingMux sync.Mutex
pendingLimits map[model.MsgCounterType]*spineapi.Message

pendingDeviceConfigMux sync.Mutex
pendingDeviceConfigs 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
Expand Down Expand Up @@ -74,8 +78,9 @@ 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),
}

_ = spine.Events.Subscribe(uc)
Expand Down Expand Up @@ -172,6 +177,80 @@ func (e *LPC) loadControlWriteCB(msg *spineapi.Message) {
go e.approveOrDenyConsumptionLimit(msg, true, "")
}

func (e *LPC) approveOrDenyDeviceConfiguration(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 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 {
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
}

// 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()
}
}

// If neither a failsafe duration nor a failsafe limit were set this message does not pertain to this callback so we accept
go e.approveOrDenyDeviceConfiguration(msg, true, "")
}

func (e *LPC) AddFeatures() {
// client features
_ = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient)
Expand Down Expand Up @@ -209,6 +288,7 @@ 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(
Expand Down
54 changes: 54 additions & 0 deletions usecases/cs/lpp/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,60 @@ func (e *LPP) SetFailsafeDurationMinimum(duration time.Duration, changeable bool
return dc.UpdateKeyValueDataForFilter(data, nil, filter)
}

// 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()

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
}

// accept or deny an incoming device configuration write
//
// 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.pendingDeviceConfigs[msgCounter]
if !ok {
// no pending limit for this msgCounter, this is a caller error
return
}

e.approveOrDenyDeviceConfiguration(msg, approve, reason)

delete(e.pendingDeviceConfigs, msgCounter)
}

// Scenario 3

// start sending heartbeat from the local entity supporting this usecase
Expand Down
14 changes: 8 additions & 6 deletions usecases/cs/lpp/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,33 @@ 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
// Controllable System data update received
//
// 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
// specified in this Use Case permit leaving the "failsafe state" data update received
//
// Use `FailsafeDurationMinimum` to get the current data
//
// Use Case LPC, Scenario 2
// Use Case LPP, Scenario 2
DataUpdateFailsafeDurationMinimum api.EventType = "cs-lpp-DataUpdateFailsafeDurationMinimum"

// Indicates a notify heartbeat event the application should care of.
Expand Down
Loading