Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: add write approval callbacks for Failsafe values of CS-LPC usecase #150

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
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
30 changes: 27 additions & 3 deletions examples/hems/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,24 @@ 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, 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 {
fmt.Println("New LPC Limit set to", currentLimit.Value, "W")
Expand All @@ -172,12 +184,24 @@ 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, 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 {
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]*DeviceConfigurations

// 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]*DeviceConfigurations

// 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
8 changes: 8 additions & 0 deletions usecases/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import (
"time"

"github.com/enbility/spine-go/model"

Check failure on line 6 in usecases/api/types.go

View workflow job for this annotation

GitHub Actions / Build

File is not properly formatted (gofmt)
spineapi "github.com/enbility/spine-go/api"
)

type EVChargeStateType string
Expand Down Expand Up @@ -166,3 +167,10 @@
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
}
26 changes: 26 additions & 0 deletions usecases/cs/lpc/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,32 @@ 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.DeviceConfigurations {
e.pendingDeviceConfigMux.Lock()
defer e.pendingDeviceConfigMux.Unlock()

return e.pendingDeviceConfigs
}

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

config, ok := e.pendingDeviceConfigs[msgCounter]
if !ok {
// no pending limit for this msgCounter, this is a caller error
return
}

e.approveOrDenyDeviceConfiguration(config.Msg, approve, reason)

delete(e.pendingDeviceConfigs, msgCounter)
}

// Scenario 3

// start sending heartbeat from the local entity supporting this usecase
Expand Down
91 changes: 90 additions & 1 deletion usecases/cs/lpc/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"sync"
"time"

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

pendingDeviceConfigMux sync.Mutex
pendingDeviceConfigs map[model.MsgCounterType]*ucapi.DeviceConfigurations

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 @@ -76,6 +80,7 @@
uc := &LPC{
UseCaseBase: usecase,
pendingLimits: make(map[model.MsgCounterType]*spineapi.Message),
pendingDeviceConfigs: make(map[model.MsgCounterType]*ucapi.DeviceConfigurations),
}

_ = spine.Events.Subscribe(uc)
Expand Down Expand Up @@ -172,6 +177,89 @@
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 len(data.DeviceConfigurationKeyValueData) == 0 || data.DeviceConfigurationKeyValueData[0].KeyId == nil {
logging.Log().Debug("LPC deviceConfigurationWriteCB: no data")
return
}

dc, err := server.NewDeviceConfiguration(e.LocalEntity)
if err != nil {
return
}

var failsafeLimit *float64 = nil
var failsafeDuration *time.Duration = nil
for _, deviceKeyValueData := range data.DeviceConfigurationKeyValueData {

Check failure on line 220 in usecases/cs/lpc/usecase.go

View workflow job for this annotation

GitHub Actions / Build

unnecessary leading newline (whitespace)

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,
}
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, "")
}
}

func (e *LPC) AddFeatures() {
// client features
_ = e.LocalEntity.GetOrAddFeature(model.FeatureTypeTypeDeviceDiagnosis, model.RoleTypeClient)
Expand Down Expand Up @@ -209,7 +297,8 @@
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{
Expand Down
26 changes: 26 additions & 0 deletions usecases/cs/lpp/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,32 @@ 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.DeviceConfigurations {
e.pendingDeviceConfigMux.Lock()
defer e.pendingDeviceConfigMux.Unlock()

return e.pendingDeviceConfigs
}

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

config, ok := e.pendingDeviceConfigs[msgCounter]
if !ok {
// no pending limit for this msgCounter, this is a caller error
return
}

e.approveOrDenyDeviceConfiguration(config.Msg, approve, reason)

delete(e.pendingDeviceConfigs, msgCounter)
}

// Scenario 3

// start sending heartbeat from the local entity supporting this usecase
Expand Down
Loading
Loading