Skip to content
This repository was archived by the owner on Jun 29, 2024. It is now read-only.

Commit e388157

Browse files
Add write approval API to UseCases implementing server side role (#37)
- Add write approval API to UCLPCServer, UCLPPServer - Add event type to notify about a new incoming use case specific write message - Add public methods to get the current list of pending writes and to approve or deny the write message Fixes #36
2 parents db87fb3 + 2bafe21 commit e388157

14 files changed

+545
-47
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.21.1
55
require (
66
github.com/enbility/eebus-go v0.0.0-20240516175253-e0841966938b
77
github.com/enbility/ship-go v0.0.0-20240512152836-f8ae5a3899f0
8-
github.com/enbility/spine-go v0.0.0-20240516174755-b3c1a8a73d93
8+
github.com/enbility/spine-go v0.0.0-20240518100904-8a8dfc01cb3c
99
github.com/stretchr/testify v1.8.4
1010
)
1111

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ github.com/enbility/eebus-go v0.0.0-20240516175253-e0841966938b h1:8WdMCIKnB289r
77
github.com/enbility/eebus-go v0.0.0-20240516175253-e0841966938b/go.mod h1:1jzTHrSftixngak1cpaLIwjMJT62qJcKYDvnUS0wB94=
88
github.com/enbility/ship-go v0.0.0-20240512152836-f8ae5a3899f0 h1:iPs4/u/N5qf3oiRHPK0tBOAr0N2vQzHV28lPe1U9iHE=
99
github.com/enbility/ship-go v0.0.0-20240512152836-f8ae5a3899f0/go.mod h1:ovyrJE3oPnGT5+eQnOqWut80gFDQ0XHn3ZWU2fHV9xQ=
10-
github.com/enbility/spine-go v0.0.0-20240516174755-b3c1a8a73d93 h1:ra+Eom9EQD9YN85WqZ9rmSTxWDi6j1R4SzHx+gHQCqs=
11-
github.com/enbility/spine-go v0.0.0-20240516174755-b3c1a8a73d93/go.mod h1:2SXeC20kPX23mTnsudvPq9qprgo7GKDiNiVdX0ebovw=
10+
github.com/enbility/spine-go v0.0.0-20240518100904-8a8dfc01cb3c h1:bX0Ohq5ldOMNjOoi+/fCqE6vVDpoZsr3oW6fFsHp1t4=
11+
github.com/enbility/spine-go v0.0.0-20240518100904-8a8dfc01cb3c/go.mod h1:2SXeC20kPX23mTnsudvPq9qprgo7GKDiNiVdX0ebovw=
1212
github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b h1:sg3c6LJ4eWffwtt9SW0lgcIX4Oh274vwdJnNFNNrDco=
1313
github.com/enbility/zeroconf/v2 v2.0.0-20240210101930-d0004078577b/go.mod h1:BjzRRiYX6mWdOgku1xxDE+NsV8PijTby7Q7BkYVdfDU=
1414
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=

uclpcserver/api.go

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"time"
55

66
"github.com/enbility/cemd/api"
7+
"github.com/enbility/spine-go/model"
78
)
89

910
//go:generate mockery
@@ -27,6 +28,17 @@ type UCLPCServerInterface interface {
2728
// set the current loadcontrol limit data
2829
SetConsumptionLimit(limit api.LoadLimit) (resultErr error)
2930

31+
// return the currently pending incoming consumption write limits
32+
PendingConsumptionLimits() map[model.MsgCounterType]api.LoadLimit
33+
34+
// accept or deny an incoming consumption write limit
35+
//
36+
// parameters:
37+
// - msg: the incoming write message
38+
// - approve: if the write limit for msg should be approved or not
39+
// - reason: the reason why the approval is denied, otherwise an empty string
40+
ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string)
41+
3042
// Scenario 2
3143

3244
// return Failsafe limit for the consumed active (real) power of the

uclpcserver/public.go

+80-20
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,12 @@ func (e *UCLPCServer) ConsumptionLimit() (limit api.LoadLimit, resultErr error)
3030
}
3131
resultErr = eebusapi.ErrDataNotAvailable
3232

33-
descriptions := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope(
34-
e.service,
35-
model.LoadControlLimitTypeTypeSignDependentAbsValueLimit,
36-
model.LoadControlCategoryTypeObligation,
37-
model.EnergyDirectionTypeConsume,
38-
model.ScopeTypeTypeActivePowerLimit,
39-
)
40-
if len(descriptions) != 1 || descriptions[0].LimitId == nil {
33+
limidId, err := e.loadControlLimitId()
34+
if err != nil {
4135
return
4236
}
43-
description := descriptions[0]
4437

45-
value := util.GetLocalLimitValueForLimitId(e.service, *description.LimitId)
38+
value := util.GetLocalLimitValueForLimitId(e.service, limidId)
4639
if value.LimitId == nil || value.Value == nil {
4740
return
4841
}
@@ -63,17 +56,10 @@ func (e *UCLPCServer) ConsumptionLimit() (limit api.LoadLimit, resultErr error)
6356
func (e *UCLPCServer) SetConsumptionLimit(limit api.LoadLimit) (resultErr error) {
6457
resultErr = eebusapi.ErrDataNotAvailable
6558

66-
descriptions := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope(
67-
e.service,
68-
model.LoadControlLimitTypeTypeSignDependentAbsValueLimit,
69-
model.LoadControlCategoryTypeObligation,
70-
model.EnergyDirectionTypeConsume,
71-
model.ScopeTypeTypeActivePowerLimit,
72-
)
73-
if len(descriptions) != 1 || descriptions[0].LimitId == nil {
59+
limidId, err := e.loadControlLimitId()
60+
if err != nil {
7461
return
7562
}
76-
description := descriptions[0]
7763

7864
localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM)
7965

@@ -83,7 +69,7 @@ func (e *UCLPCServer) SetConsumptionLimit(limit api.LoadLimit) (resultErr error)
8369
}
8470

8571
limitData := model.LoadControlLimitDataType{
86-
LimitId: description.LimitId,
72+
LimitId: eebusutil.Ptr(limidId),
8773
IsLimitChangeable: eebusutil.Ptr(limit.IsChangeable),
8874
IsLimitActive: eebusutil.Ptr(limit.IsActive),
8975
Value: model.NewScaledNumberType(limit.Value),
@@ -103,6 +89,80 @@ func (e *UCLPCServer) SetConsumptionLimit(limit api.LoadLimit) (resultErr error)
10389
return nil
10490
}
10591

92+
// return the currently pending incoming consumption write limits
93+
func (e *UCLPCServer) PendingConsumptionLimits() map[model.MsgCounterType]api.LoadLimit {
94+
result := make(map[model.MsgCounterType]api.LoadLimit)
95+
96+
limitId, err := e.loadControlLimitId()
97+
if err != nil {
98+
return result
99+
}
100+
101+
e.pendingMux.Lock()
102+
defer e.pendingMux.Unlock()
103+
104+
for key, msg := range e.pendingLimits {
105+
data := msg.Cmd.LoadControlLimitListData
106+
107+
// elements are only added to the map if all required fields exist
108+
// therefor not check for these are needed here
109+
110+
// find the item which contains the limit for this usecase
111+
for _, item := range data.LoadControlLimitData {
112+
if item.LimitId == nil ||
113+
limitId != *item.LimitId {
114+
continue
115+
}
116+
117+
limit := api.LoadLimit{}
118+
119+
if item.TimePeriod != nil {
120+
if duration, err := item.TimePeriod.GetDuration(); err == nil {
121+
limit.Duration = duration
122+
}
123+
}
124+
125+
if item.IsLimitActive != nil {
126+
limit.IsActive = *item.IsLimitActive
127+
}
128+
129+
if item.Value != nil {
130+
limit.Value = item.Value.GetValue()
131+
}
132+
133+
result[key] = limit
134+
}
135+
}
136+
137+
return result
138+
}
139+
140+
// accept or deny an incoming consumption write limit
141+
//
142+
// use PendingConsumptionLimits to get the list of currently pending requests
143+
func (e *UCLPCServer) ApproveOrDenyConsumptionLimit(msgCounter model.MsgCounterType, approve bool, reason string) {
144+
e.pendingMux.Lock()
145+
defer e.pendingMux.Unlock()
146+
147+
msg, ok := e.pendingLimits[msgCounter]
148+
if !ok {
149+
return
150+
}
151+
152+
localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM)
153+
154+
f := localEntity.FeatureOfTypeAndRole(model.FeatureTypeTypeLoadControl, model.RoleTypeServer)
155+
156+
result := model.ErrorType{
157+
ErrorNumber: model.ErrorNumberType(0),
158+
}
159+
if !approve {
160+
result.ErrorNumber = model.ErrorNumberType(7)
161+
result.Description = eebusutil.Ptr(model.DescriptionType(reason))
162+
}
163+
f.ApproveOrDenyWrite(msg, result)
164+
}
165+
106166
// Scenario 2
107167

108168
// return Failsafe limit for the consumed active (real) power of the

uclpcserver/public_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"time"
55

66
"github.com/enbility/cemd/api"
7+
eebusutil "github.com/enbility/eebus-go/util"
8+
spineapi "github.com/enbility/spine-go/api"
9+
"github.com/enbility/spine-go/model"
710
"github.com/stretchr/testify/assert"
811
)
912

@@ -26,6 +29,42 @@ func (s *UCLPCServerSuite) Test_ConsumptionLimit() {
2629
assert.Nil(s.T(), err)
2730
}
2831

32+
func (s *UCLPCServerSuite) Test_PendingConsumptionLimits() {
33+
data := s.sut.PendingConsumptionLimits()
34+
assert.Equal(s.T(), 0, len(data))
35+
36+
msgCounter := model.MsgCounterType(500)
37+
38+
msg := &spineapi.Message{
39+
RequestHeader: &model.HeaderType{
40+
MsgCounter: eebusutil.Ptr(msgCounter),
41+
},
42+
Cmd: model.CmdType{
43+
LoadControlLimitListData: &model.LoadControlLimitListDataType{
44+
LoadControlLimitData: []model.LoadControlLimitDataType{
45+
{
46+
LimitId: eebusutil.Ptr(model.LoadControlLimitIdType(0)),
47+
IsLimitActive: eebusutil.Ptr(true),
48+
Value: model.NewScaledNumberType(1000),
49+
TimePeriod: model.NewTimePeriodTypeWithRelativeEndTime(time.Minute * 2),
50+
},
51+
},
52+
},
53+
},
54+
DeviceRemote: s.remoteDevice,
55+
EntityRemote: s.monitoredEntity,
56+
}
57+
58+
s.sut.loadControlWriteCB(msg)
59+
60+
data = s.sut.PendingConsumptionLimits()
61+
assert.Equal(s.T(), 1, len(data))
62+
63+
s.sut.ApproveOrDenyConsumptionLimit(model.MsgCounterType(499), true, "")
64+
65+
s.sut.ApproveOrDenyConsumptionLimit(msgCounter, false, "leave me alone")
66+
}
67+
2968
func (s *UCLPCServerSuite) Test_Failsafe() {
3069
limit, changeable, err := s.sut.FailsafeConsumptionActivePowerLimit()
3170
assert.Equal(s.T(), 0.0, limit)

uclpcserver/types.go

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ const (
1212
// Use Case LPC, Scenario 1
1313
DataUpdateLimit api.EventType = "DataUpdateLimit"
1414

15+
// An incoming load control obligation limit needs to be approved or denied
16+
//
17+
// Use `PendingConsumptionLimits` to get the currently pending write approval requests
18+
// and invoke `ApproveOrDenyConsumptionLimit` for each
19+
//
20+
// Use Case LPC, Scenario 1
21+
WriteApprovalRequired api.EventType = "WriteApprovalRequired"
22+
1523
// Failsafe limit for the consumed active (real) power of the
1624
// Controllable System data update received
1725
//

uclpcserver/uclpc.go

+77-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package uclpcserver
22

33
import (
4+
"errors"
5+
"sync"
6+
47
"github.com/enbility/cemd/api"
58
"github.com/enbility/cemd/util"
69
eebusapi "github.com/enbility/eebus-go/api"
@@ -17,15 +20,19 @@ type UCLPCServer struct {
1720

1821
validEntityTypes []model.EntityTypeType
1922

23+
pendingMux sync.Mutex
24+
pendingLimits map[model.MsgCounterType]*spineapi.Message
25+
2026
heartbeatKeoWorkaround bool // required because KEO Stack uses multiple identical entities for the same functionality, and it is not clear which to use
2127
}
2228

2329
var _ UCLPCServerInterface = (*UCLPCServer)(nil)
2430

2531
func NewUCLPC(service eebusapi.ServiceInterface, eventCB api.EntityEventCallback) *UCLPCServer {
2632
uc := &UCLPCServer{
27-
service: service,
28-
eventCB: eventCB,
33+
service: service,
34+
eventCB: eventCB,
35+
pendingLimits: make(map[model.MsgCounterType]*spineapi.Message),
2936
}
3037

3138
uc.validEntityTypes = []model.EntityTypeType{
@@ -42,6 +49,73 @@ func (c *UCLPCServer) UseCaseName() model.UseCaseNameType {
4249
return model.UseCaseNameTypeLimitationOfPowerConsumption
4350
}
4451

52+
func (e *UCLPCServer) loadControlLimitId() (limitid model.LoadControlLimitIdType, err error) {
53+
limitid = model.LoadControlLimitIdType(0)
54+
err = errors.New("not found")
55+
56+
descriptions := util.GetLocalLimitDescriptionsForTypeCategoryDirectionScope(
57+
e.service,
58+
model.LoadControlLimitTypeTypeSignDependentAbsValueLimit,
59+
model.LoadControlCategoryTypeObligation,
60+
model.EnergyDirectionTypeConsume,
61+
model.ScopeTypeTypeActivePowerLimit,
62+
)
63+
if len(descriptions) != 1 || descriptions[0].LimitId == nil {
64+
return
65+
}
66+
description := descriptions[0]
67+
68+
if description.LimitId == nil {
69+
return
70+
}
71+
72+
return *description.LimitId, nil
73+
}
74+
75+
// callback invoked on incoming write messages to this
76+
// loadcontrol server feature.
77+
// the implementation only considers write messages for this use case and
78+
// approves all others
79+
func (e *UCLPCServer) loadControlWriteCB(msg *spineapi.Message) {
80+
e.pendingMux.Lock()
81+
defer e.pendingMux.Unlock()
82+
83+
if msg.RequestHeader == nil || msg.RequestHeader.MsgCounter == nil ||
84+
msg.Cmd.LoadControlLimitListData == nil {
85+
return
86+
}
87+
88+
limitId, err := e.loadControlLimitId()
89+
if err != nil {
90+
return
91+
}
92+
93+
data := msg.Cmd.LoadControlLimitListData
94+
95+
// we assume there is always only one limit
96+
if data == nil || data.LoadControlLimitData == nil ||
97+
len(data.LoadControlLimitData) == 0 {
98+
return
99+
}
100+
101+
// check if there is a matching limitId in the data
102+
for _, item := range data.LoadControlLimitData {
103+
if item.LimitId == nil ||
104+
limitId != *item.LimitId {
105+
continue
106+
}
107+
108+
if _, ok := e.pendingLimits[*msg.RequestHeader.MsgCounter]; !ok {
109+
e.pendingLimits[*msg.RequestHeader.MsgCounter] = msg
110+
e.eventCB(msg.DeviceRemote.Ski(), msg.DeviceRemote, msg.EntityRemote, WriteApprovalRequired)
111+
return
112+
}
113+
}
114+
115+
// approve, because this is no request for this usecase
116+
go e.ApproveOrDenyConsumptionLimit(*msg.RequestHeader.MsgCounter, true, "")
117+
}
118+
45119
func (e *UCLPCServer) AddFeatures() {
46120
localEntity := e.service.LocalDevice().EntityForType(model.EntityTypeTypeCEM)
47121

@@ -52,6 +126,7 @@ func (e *UCLPCServer) AddFeatures() {
52126
f := localEntity.GetOrAddFeature(model.FeatureTypeTypeLoadControl, model.RoleTypeServer)
53127
f.AddFunctionType(model.FunctionTypeLoadControlLimitDescriptionListData, true, false)
54128
f.AddFunctionType(model.FunctionTypeLoadControlLimitListData, true, true)
129+
_ = f.AddWriteApprovalCallback(e.loadControlWriteCB)
55130

56131
var limitId model.LoadControlLimitIdType = 0
57132
// get the highest limitId

0 commit comments

Comments
 (0)