Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/evse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
38 changes: 33 additions & 5 deletions examples/hems/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,28 @@ 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()

// 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, "")
}
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 {
if config.Description.KeyName != nil {
fmt.Printf("%s ", string(*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 @@ -169,15 +182,30 @@ 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()

// 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, "")
}
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 {
if config.Description.KeyName != nil {
fmt.Printf("%s ", string(*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"`
}
26 changes: 26 additions & 0 deletions usecases/cs/lpc/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -270,6 +271,31 @@ 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 {
e.pendingDeviceConfigMux.Lock()
defer e.pendingDeviceConfigMux.Unlock()

return internal.GroupPendingDeviceConfigurations(e.pendingDeviceConfigs, e.LocalEntity)
}

// 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
43 changes: 43 additions & 0 deletions usecases/cs/lpc/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
14 changes: 11 additions & 3 deletions usecases/cs/lpc/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ 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` to get the currently pending consumption limits
// awaiting approval 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 configurations
// awaiting approval 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
Expand Down
56 changes: 53 additions & 3 deletions usecases/cs/lpc/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,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 @@ -77,8 +80,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 @@ -165,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
}
}
Expand All @@ -175,6 +179,51 @@ 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) {
configsToApprove := map[model.DeviceConfigurationKeyNameType]struct{}{
model.DeviceConfigurationKeyNameTypeFailsafeConsumptionActivePowerLimit: {},
model.DeviceConfigurationKeyNameTypeFailsafeDurationMinimum: {},
}
approvalRequired, err := internal.ConfigurationWriteRequiresApproval(msg, e.LocalEntity, configsToApprove)
if err != nil {
logging.Log().Errorf("LPC deviceConfigurationWriteCB: %s", err.Error())
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 use case 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 @@ -213,6 +262,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
Loading
Loading