diff --git a/README.md b/README.md index 860e16b4..28172575 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Planned milestones and features: - [x] OCPP 1.6 - [x] OCPP 2.0.1 (examples working, but will need more real-world testing) -- [ ] Dedicated package for configuration management +- [x] Dedicated package for configuration management (OCPP 1.6 supported) ## OCPP 1.6 Usage @@ -518,6 +518,71 @@ if err != nil { Or you may build requests manually and send them using either the synchronous or asynchronous API. +### OCPP configuration manager (OCPP 1.6) + +```go +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" + log "github.com/sirupsen/logrus" + configManager "github.com/lorenzodonini/ocpp-go/ocpp1.6/ocpp_v16_config_manager" +) + +func main() { + log.SetLevel(log.DebugLevel) + + supportedProfiles := []string{core.ProfileName, smartcharging.ProfileName} + defaultConfig, err := configManager.DefaultConfiguration(supportedProfiles...) + if err != nil { + log.Errorf("Error getting default configuration: %v", err) + return + } + + manager, err := configManager.NewV16ConfigurationManager(defaultConfig, supportedProfiles...) + + // Get value + value, err := manager.GetConfigurationValue(configManager.AuthorizeRemoteTxRequests) + if err != nil { + log.Errorf("Error getting configuration value: %v", err) + return + } + + log.Println(*value) + + // Update key + val := "false" + err = manager.UpdateKey(ocpp_v16.AuthorizeRemoteTxRequests, &val) + if err != nil { + log.Errorf("Error updating key: %v", err) + return + } + + // Get value + value, err = manager.GetConfigurationValue(configManager.AuthorizeRemoteTxRequests) + if err != nil { + log.Errorf("Error getting configuration value: %v", err) + return + } + + log.Println(*value) + + // Register custom key validator, which will prevent the key from being updated + manager.RegisterCustomKeyValidator(func(key ocpp_v16.Key, value *string) bool { + return key != ocpp_v16.AuthorizeRemoteTxRequests + }) + + // Update key + val = "true" + err = manager.UpdateKey(configManager.AuthorizeRemoteTxRequests, &val) + if err != nil { + log.Errorf("Error updating key: %v", err) + return + } +} +``` + #### Contributing If you're contributing a code change, you'll want to be sure the tests are passing first; here are the steps to check that: diff --git a/go.mod b/go.mod index e1761106..16c9bd81 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/Shopify/toxiproxy v2.1.4+incompatible + github.com/agrison/go-commons-lang v0.0.0-20240106075236-2e001e6401ef github.com/go-playground/locales v0.12.1 // indirect github.com/go-playground/universal-translator v0.16.0 github.com/gorilla/mux v1.7.3 @@ -11,9 +12,9 @@ require ( github.com/kr/pretty v0.1.0 // indirect github.com/leodido/go-urn v1.1.0 // indirect github.com/relvacode/iso8601 v1.3.0 + github.com/samber/lo v1.46.0 github.com/sirupsen/logrus v1.4.2 github.com/stretchr/testify v1.8.0 - golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v9 v9.30.0 diff --git a/go.sum b/go.sum index b1223cf9..227e2b7f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/agrison/go-commons-lang v0.0.0-20240106075236-2e001e6401ef h1:KkznClyESbRaLmRo7Oam4vv5L4oknDK+mixJ9mypl6E= +github.com/agrison/go-commons-lang v0.0.0-20240106075236-2e001e6401ef/go.mod h1:u+Zwm0OKtJAGx+DXcmp2NNwZ0GKtV80ipbF/uhKhQdw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,6 +9,7 @@ github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotf github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= @@ -24,6 +27,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -34,9 +39,68 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= -golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/ocpp1.6/config_manager/configuration.go b/ocpp1.6/config_manager/configuration.go new file mode 100644 index 00000000..74f7d783 --- /dev/null +++ b/ocpp1.6/config_manager/configuration.go @@ -0,0 +1,110 @@ +package ocpp_16_config_manager + +import ( + "errors" + "fmt" + "strings" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/samber/lo" +) + +var ( + ErrKeyNotFound = errors.New("key not found") + ErrReadOnly = errors.New("attribute is read-only") +) + +type Key string + +func (k Key) String() string { + return string(k) +} + +type Config struct { + Version int `fig:"version" default:"1"` + Keys []core.ConfigurationKey `fig:"keys"` +} + +// UpdateKey Update the configuration variable in the configuration if it is not readonly. +func (config *Config) UpdateKey(key string, value *string) error { + // Find the index of the key + configKey, index, isFound := lo.FindIndexOf(config.Keys, func(item core.ConfigurationKey) bool { + return item.Key == key + }) + if !isFound { + return ErrKeyNotFound + } + + if configKey.Readonly { + return ErrReadOnly + } + + config.Keys[index].Value = value + return nil +} + +// UpdateKeyReadability updates whether the key is updatable or not. +func (config *Config) UpdateKeyReadability(key string, readable bool) error { + // Find the index of the key + _, index, isFound := lo.FindIndexOf(config.Keys, func(item core.ConfigurationKey) bool { + return item.Key == key + }) + if !isFound { + return ErrKeyNotFound + } + + config.Keys[index].Readonly = readable + return nil +} + +// GetConfigurationValue Get the value of specified configuration variable in String format. +func (config *Config) GetConfigurationValue(key string) (*string, error) { + configKey, isFound := lo.Find(config.Keys, func(item core.ConfigurationKey) bool { + return item.Key == key + }) + + if !isFound { + return nil, ErrKeyNotFound + } + + return configKey.Value, nil +} + +// GetConfig Get the configuration +func (config *Config) GetConfig() []core.ConfigurationKey { + return config.Keys +} + +// GetVersion Get the current version +func (config *Config) GetVersion() int { + return config.Version +} + +// SetVersion Set the current version +func (config *Config) SetVersion(version int) { + config.Version = version +} + +// Validate validates the configuration - check if all mandatory keys are present. +func (config *Config) Validate(mandatoryKeys []Key) error { + missingKeys := "" + + containsMandatoryKeys := true + + for _, key := range mandatoryKeys { + containsKey := lo.ContainsBy(config.Keys, func(item core.ConfigurationKey) bool { + return item.Key == key.String() + }) + + if !containsKey { + missingKeys = strings.Join([]string{missingKeys, key.String()}, ", ") + containsMandatoryKeys = false + } + } + + if !containsMandatoryKeys { + return fmt.Errorf("missing mandatory keys: %s", missingKeys) + } + + return nil +} diff --git a/ocpp1.6/config_manager/configuration_test.go b/ocpp1.6/config_manager/configuration_test.go new file mode 100644 index 00000000..da234321 --- /dev/null +++ b/ocpp1.6/config_manager/configuration_test.go @@ -0,0 +1,151 @@ +package ocpp_16_config_manager + +import ( + "testing" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/samber/lo" + "github.com/stretchr/testify/suite" +) + +var ( + val1 = "60" + val2 = "ABCD" +) + +type OcppConfigTest struct { + suite.Suite + config Config +} + +func (s *OcppConfigTest) SetupTest() { + s.config = Config{ + Version: 1, + Keys: []core.ConfigurationKey{ + { + Key: "HeartbeatInterval", + Readonly: false, + Value: &val1, + }, { + Key: "ChargingScheduleAllowedChargingRateUnit", + Readonly: true, + Value: &val2, + }, { + Key: "AuthorizationCacheEnabled", + Readonly: false, + Value: nil, + }, + }, + } +} + +func (s *OcppConfigTest) TestGetConfig() { + s.Assert().Equal([]core.ConfigurationKey{ + { + Key: "HeartbeatInterval", + Readonly: false, + Value: &val1, + }, { + Key: "ChargingScheduleAllowedChargingRateUnit", + Readonly: true, + Value: &val2, + }, { + Key: "AuthorizationCacheEnabled", + Readonly: false, + Value: nil, + }, + }, s.config.GetConfig()) + + // Overwrite the config + s.config = Config{ + Version: 1, + Keys: []core.ConfigurationKey{}, + } + + s.Assert().Equal([]core.ConfigurationKey{}, s.config.GetConfig()) +} + +func (s *OcppConfigTest) TestGetConfigurationValue() { + // Ok case + value, err := s.config.GetConfigurationValue(HeartbeatInterval.String()) + s.Require().NoError(err) + s.Assert().EqualValues("60", *value) + + // Invalid key + value, err = s.config.GetConfigurationValue("Test4") + s.Assert().Error(err) + s.Assert().Nil(value) +} + +func (s *OcppConfigTest) TestGetVersion() { + s.Assert().EqualValues(1, s.config.GetVersion()) + + s.config.Version = 1234 + + s.Assert().EqualValues(1234, s.config.GetVersion()) +} + +func (s *OcppConfigTest) TestSetVersion() { + s.config.SetVersion(1234) + s.Assert().EqualValues(1234, s.config.Version) + + s.config.SetVersion(1) + s.Assert().EqualValues(1, s.config.Version) +} + +func (s *OcppConfigTest) TestUpdateKey() { + // Ok case + newVal := "1234" + err := s.config.UpdateKey("HeartbeatInterval", &newVal) + s.Assert().NoError(err) + value, err := s.config.GetConfigurationValue("HeartbeatInterval") + s.Require().NoError(err) + s.Assert().EqualValues("1234", *value) + + // Invalid key + err = s.config.UpdateKey("Test4", nil) + s.Assert().Error(err) + + // Key cannot be updated + err = s.config.UpdateKey("ChargingScheduleAllowedChargingRateUnit", nil) + s.Assert().Error(err) + + // Check if the value was not updated + value, err = s.config.GetConfigurationValue("ChargingScheduleAllowedChargingRateUnit") + s.Assert().NoError(err) + s.Assert().EqualValues("ABCD", *value) +} + +func (s *OcppConfigTest) TestUpdateKeyReadability() { + // Ok case + err := s.config.UpdateKeyReadability(HeartbeatInterval.String(), true) + s.Assert().NoError(err) + + configKey, isFound := lo.Find(s.config.Keys, func(item core.ConfigurationKey) bool { + return item.Key == HeartbeatInterval.String() + }) + s.Assert().True(isFound) + s.Assert().EqualValues(true, configKey.Readonly) + + // Invalid key + err = s.config.UpdateKeyReadability("Test4", true) + s.Assert().Error(err) +} + +func (s *OcppConfigTest) TestValidate() { + s.config = NewEmptyConfiguration() + s.config.Keys = DefaultCoreConfiguration() + + // Ok case + err := s.config.Validate(MandatoryCoreKeys) + s.Assert().NoError(err) + + // Missing mandatory key + s.config.Keys = s.config.Keys[:len(s.config.Keys)-2] + err = s.config.Validate(MandatoryCoreKeys) + s.Assert().Error(err) +} + +func TestOCPPConfig(t *testing.T) { + suite.Run(t, new(OcppConfigTest)) +} diff --git a/ocpp1.6/config_manager/defaults.go b/ocpp1.6/config_manager/defaults.go new file mode 100644 index 00000000..0742f282 --- /dev/null +++ b/ocpp1.6/config_manager/defaults.go @@ -0,0 +1,226 @@ +package ocpp_16_config_manager + +import ( + "fmt" + "strings" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" + "github.com/samber/lo" +) + +func NewEmptyConfiguration() Config { + return Config{ + Version: 1, + Keys: []core.ConfigurationKey{}, + } +} + +func DefaultConfigurationFromProfiles(profiles ...string) (*Config, error) { + keys := []core.ConfigurationKey{} + + if len(profiles) == 0 { + return nil, fmt.Errorf("no profiles provided") + } + + for _, profile := range profiles { + switch profile { + case core.ProfileName: + keys = append(keys, DefaultCoreConfiguration()...) + case localauth.ProfileName: + keys = append(keys, DefaultLocalAuthConfiguration()...) + case smartcharging.ProfileName: + keys = append(keys, DefaultSmartChargingConfiguration()...) + case firmware.ProfileName: + keys = append(keys, DefaultFirmwareConfiguration()...) + default: + return nil, fmt.Errorf("unknown profile %v", profile) + } + } + + return &Config{ + Version: 1, + Keys: keys, + }, nil +} + +func DefaultCoreConfiguration() []core.ConfigurationKey { + return []core.ConfigurationKey{ + { + Key: AuthorizeRemoteTxRequests.String(), + Readonly: false, + Value: lo.ToPtr("true"), + }, + { + Key: ClockAlignedDataInterval.String(), + Readonly: false, + Value: lo.ToPtr("0"), + }, + { + Key: ConnectionTimeOut.String(), + Readonly: false, + Value: lo.ToPtr("60"), + }, + { + Key: GetConfigurationMaxKeys.String(), + Readonly: false, + Value: lo.ToPtr("100"), + }, + { + Key: HeartbeatInterval.String(), + Readonly: false, + Value: lo.ToPtr("60"), + }, + { + Key: LocalPreAuthorize.String(), + Readonly: false, + Value: lo.ToPtr("false"), + }, + { + Key: MeterValuesAlignedData.String(), + Readonly: false, + Value: lo.ToPtr("true"), + }, + { + Key: MeterValuesSampledData.String(), + Readonly: false, + Value: lo.ToPtr(strings.Join([]string{ + string(types.MeasurandVoltage), + string(types.MeasurandCurrentImport), + string(types.MeasurandPowerActiveImport), + string(types.MeasurandEnergyActiveImportInterval), + string(types.MeasueandSoC), + }, ",")), + }, + { + Key: MeterValueSampleInterval.String(), + Readonly: false, + Value: lo.ToPtr("20"), + }, + { + Key: NumberOfConnectors.String(), + Readonly: true, + Value: lo.ToPtr("1"), + }, + { + Key: ResetRetries.String(), + Readonly: false, + Value: lo.ToPtr("3"), + }, + { + Key: ConnectorPhaseRotation.String(), + Readonly: true, + Value: lo.ToPtr("Unknown"), + }, + { + Key: StopTransactionOnEVSideDisconnect.String(), + Readonly: false, + Value: lo.ToPtr("true"), + }, + { + Key: StopTransactionOnInvalidId.String(), + Readonly: false, + Value: lo.ToPtr("true"), + }, + { + Key: StopTxnAlignedData.String(), + Readonly: false, + Value: lo.ToPtr(strings.Join([]string{ + string(types.MeasurandVoltage), + string(types.MeasurandCurrentImport), + string(types.MeasurandPowerActiveImport), + string(types.MeasurandEnergyActiveImportInterval), + string(types.MeasueandSoC), + }, ",")), + }, + { + Key: StopTxnSampledData.String(), + Readonly: false, + Value: lo.ToPtr(strings.Join([]string{ + string(types.MeasurandVoltage), + string(types.MeasurandCurrentImport), + string(types.MeasurandPowerActiveImport), + string(types.MeasurandEnergyActiveImportInterval), + string(types.MeasueandSoC), + }, ",")), + }, + { + Key: SupportedFeatureProfiles.String(), + Readonly: true, + Value: lo.ToPtr("Core"), + }, + { + Key: TransactionMessageAttempts.String(), + Readonly: false, + Value: lo.ToPtr("3"), + }, + { + Key: TransactionMessageRetryInterval.String(), + Readonly: false, + Value: lo.ToPtr("30"), + }, + { + Key: UnlockConnectorOnEVSideDisconnect.String(), + Readonly: false, + Value: lo.ToPtr("true"), + }, + } +} + +func DefaultLocalAuthConfiguration() []core.ConfigurationKey { + return []core.ConfigurationKey{ + { + Key: LocalAuthListEnabled.String(), + Readonly: false, + Value: lo.ToPtr("true"), + }, + { + Key: LocalAuthListMaxLength.String(), + Readonly: true, + Value: lo.ToPtr("100"), + }, + { + Key: SendLocalListMaxLength.String(), + Readonly: true, + Value: lo.ToPtr("100"), + }, + } +} + +func DefaultSmartChargingConfiguration() []core.ConfigurationKey { + return []core.ConfigurationKey{ + { + Key: ChargeProfileMaxStackLevel.String(), + Readonly: true, + Value: lo.ToPtr("5"), + }, + { + Key: ChargingScheduleAllowedChargingRateUnit.String(), + Readonly: true, + Value: lo.ToPtr("Current,Power"), + }, + { + Key: ChargingScheduleMaxPeriods.String(), + Readonly: true, + Value: lo.ToPtr("6"), + }, + { + Key: MaxChargingProfilesInstalled.String(), + Readonly: true, + Value: lo.ToPtr("5"), + }, + } +} + +func DefaultFirmwareConfiguration() []core.ConfigurationKey { + return []core.ConfigurationKey{ + { + Key: SupportedFileTransferProtocols.String(), + Readonly: true, + Value: lo.ToPtr("HTTP,HTTPS,FTP,FTPS,SFTP"), + }, + } +} diff --git a/ocpp1.6/config_manager/defaults_test.go b/ocpp1.6/config_manager/defaults_test.go new file mode 100644 index 00000000..c07da810 --- /dev/null +++ b/ocpp1.6/config_manager/defaults_test.go @@ -0,0 +1,66 @@ +package ocpp_16_config_manager + +import ( + "testing" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" + "github.com/stretchr/testify/suite" +) + +type defaultsTestSuite struct { + suite.Suite +} + +func (suite *defaultsTestSuite) TestNewEmptyConfiguration() { + config := NewEmptyConfiguration() + suite.Equal(1, config.Version) + suite.Empty(config.Keys) +} + +func (suite *defaultsTestSuite) TestDefaultConfiguration() { + + // Default configuration with core, localauth and smartcharging profiles + keys := append(DefaultCoreConfiguration(), DefaultLocalAuthConfiguration()...) + keys = append(keys, DefaultSmartChargingConfiguration()...) + + config, err := DefaultConfigurationFromProfiles(core.ProfileName, localauth.ProfileName, smartcharging.ProfileName) + suite.NoError(err) + suite.Equal(1, config.Version) + suite.NotEmpty(config.Keys) + suite.ElementsMatch(keys, config.Keys) + + // Default configuration with unknown profile + config, err = DefaultConfigurationFromProfiles(core.ProfileName, localauth.ProfileName, smartcharging.ProfileName, "unknown") + suite.Error(err) + suite.Nil(config) + + config, err = DefaultConfigurationFromProfiles() + suite.Error(err) + suite.Nil(config) +} + +func (suite *defaultsTestSuite) TestDefaultCoreConfiguration() { + config := DefaultCoreConfiguration() + suite.NotEmpty(config) +} + +func (suite *defaultsTestSuite) TestDefaultLocalAuthConfiguration() { + config := DefaultLocalAuthConfiguration() + suite.NotEmpty(config) +} + +func (suite *defaultsTestSuite) TestDefaultSmartChargingConfiguration() { + config := DefaultSmartChargingConfiguration() + suite.NotEmpty(config) +} + +func (suite *defaultsTestSuite) TestDefaultFirmwareConfiguration() { + config := DefaultFirmwareConfiguration() + suite.NotEmpty(config) +} + +func TestDefaultConfigurations(t *testing.T) { + suite.Run(t, new(defaultsTestSuite)) +} diff --git a/ocpp1.6/config_manager/keys.go b/ocpp1.6/config_manager/keys.go new file mode 100644 index 00000000..694c6266 --- /dev/null +++ b/ocpp1.6/config_manager/keys.go @@ -0,0 +1,153 @@ +package ocpp_16_config_manager + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" +) + +const ( + /* ----------------- Core keys ----------------------- */ + + AllowOfflineTxForUnknownId = Key("AllowOfflineTxForUnknownId") + AuthorizationCacheEnabled = Key("AuthorizationCacheEnabled") + AuthorizeRemoteTxRequests = Key("AuthorizeRemoteTxRequests") + BlinkRepeat = Key("BlinkRepeat") + ClockAlignedDataInterval = Key("ClockAlignedDataInterval") + ConnectionTimeOut = Key("ConnectionTimeOut") + GetConfigurationMaxKeys = Key("GetConfigurationMaxKeys") + HeartbeatInterval = Key("HeartbeatInterval") + LightIntensity = Key("LightIntensity") + LocalAuthorizeOffline = Key("LocalAuthorizeOffline") + LocalPreAuthorize = Key("LocalPreAuthorize") + MaxEnergyOnInvalidId = Key("MaxEnergyOnInvalidId") + MeterValuesAlignedData = Key("MeterValuesAlignedData") + MeterValuesAlignedDataMaxLength = Key("MeterValuesAlignedDataMaxLength") + MeterValuesSampledData = Key("MeterValuesSampledData") + MeterValuesSampledDataMaxLength = Key("MeterValuesSampledDataMaxLength") + MeterValueSampleInterval = Key("MeterValueSampleInterval") + MinimumStatusDuration = Key("MinimumStatusDuration") + NumberOfConnectors = Key("NumberOfConnectors") + ResetRetries = Key("ResetRetries") + ConnectorPhaseRotation = Key("ConnectorPhaseRotation") + ConnectorPhaseRotationMaxLength = Key("ConnectorPhaseRotationMaxLength") + StopTransactionOnEVSideDisconnect = Key("StopTransactionOnEVSideDisconnect") + StopTransactionOnInvalidId = Key("StopTransactionOnInvalidId") + StopTxnAlignedData = Key("StopTxnAlignedData") + StopTxnAlignedDataMaxLength = Key("StopTxnAlignedDataMaxLength") + StopTxnSampledData = Key("StopTxnSampledData") + StopTxnSampledDataMaxLength = Key("StopTxnSampledDataMaxLength") + SupportedFeatureProfiles = Key("SupportedFeatureProfiles") + SupportedFeatureProfilesMaxLength = Key("SupportedFeatureProfilesMaxLength") + TransactionMessageAttempts = Key("TransactionMessageAttempts") + TransactionMessageRetryInterval = Key("TransactionMessageRetryInterval") + UnlockConnectorOnEVSideDisconnect = Key("UnlockConnectorOnEVSideDisconnect") + WebSocketPingInterval = Key("WebSocketPingInterval") + + /* ----------------- LocalAuthList keys ----------------------- */ + + LocalAuthListEnabled = Key("LocalAuthListEnabled") + LocalAuthListMaxLength = Key("LocalAuthListMaxLength") + SendLocalListMaxLength = Key("SendLocalListMaxLength") + + /* ----------------- Reservation keys ----------------------- */ + + ReserveConnectorZeroSupported = Key("ReserveConnectorZeroSupported") + + /* ----------------- Firmware keys ----------------------- */ + + SupportedFileTransferProtocols = Key("SupportedFileTransferProtocols") + + /* ----------------- SmartCharging keys ----------------------- */ + + ChargeProfileMaxStackLevel = Key("ChargeProfileMaxStackLevel") + ChargingScheduleAllowedChargingRateUnit = Key("ChargingScheduleAllowedChargingRateUnit") + ChargingScheduleMaxPeriods = Key("ChargingScheduleMaxPeriods") + MaxChargingProfilesInstalled = Key("MaxChargingProfilesInstalled") + ConnectorSwitch3to1PhaseSupported = Key("ConnectorSwitch3to1PhaseSupported") + + /* ----------------- ISO15118 keys ----------------------- */ + CentralContractValidationAllowed = Key("CentralContractValidationAllowed") + CertificateSignedMaxChainSize = Key("CertificateSignedMaxChainSize") + CertSigningWaitMinimum = Key("CertSigningWaitMinimum") + CertSigningRepeatTimes = Key("CertSigningRepeatTimes") + CertificateStoreMaxLength = Key("CertificateStoreMaxLength") + ContractValidationOffline = Key("ContractValidationOffline") + ISO15118PnCEnabled = Key("ISO15118PnCEnabled") + + /* ----------------- Security extension keys ----------------------- */ + AuthorizationData = Key("AuthorizationData") + AdditionalRootCertificateCheck = Key("AdditionalRootCertificateCheck") + CpoName = Key("CpoName") + SecurityProfile = Key("SecurityProfile") +) + +var ( + MandatoryCoreKeys = []Key{ + AuthorizeRemoteTxRequests, + ClockAlignedDataInterval, + ConnectionTimeOut, + GetConfigurationMaxKeys, + HeartbeatInterval, + LocalPreAuthorize, + MeterValuesAlignedData, + MeterValuesSampledData, + MeterValueSampleInterval, + NumberOfConnectors, + ResetRetries, + ConnectorPhaseRotation, + StopTransactionOnEVSideDisconnect, + StopTransactionOnInvalidId, + StopTxnAlignedData, + StopTxnSampledData, + SupportedFeatureProfiles, + TransactionMessageAttempts, + TransactionMessageRetryInterval, + UnlockConnectorOnEVSideDisconnect, + } + + MandatoryLocalAuthKeys = []Key{ + LocalAuthListEnabled, + LocalAuthListMaxLength, + SendLocalListMaxLength, + } + + MandatorySmartChargingKeys = []Key{ + MaxChargingProfilesInstalled, + ChargingScheduleMaxPeriods, + ChargingScheduleAllowedChargingRateUnit, + ChargeProfileMaxStackLevel, + } + + MandatoryFirmwareKeys = []Key{ + SupportedFileTransferProtocols, + } + + MandatoryISO15118Keys = []Key{ + ISO15118PnCEnabled, + ContractValidationOffline, + } + + // Security extension does not have any mandatory keys +) + +func GetMandatoryKeysForProfile(profiles ...string) []Key { + mandatoryKeys := []Key{} + + for _, profile := range profiles { + switch profile { + case core.ProfileName: + mandatoryKeys = append(mandatoryKeys, MandatoryCoreKeys...) + case smartcharging.ProfileName: + mandatoryKeys = append(mandatoryKeys, MandatorySmartChargingKeys...) + case localauth.ProfileName: + mandatoryKeys = append(mandatoryKeys, MandatoryLocalAuthKeys...) + case firmware.ProfileName: + mandatoryKeys = append(mandatoryKeys, MandatoryFirmwareKeys...) + // todo IS15118 mandatory keys validation + } + } + + return mandatoryKeys +} diff --git a/ocpp1.6/config_manager/keys_test.go b/ocpp1.6/config_manager/keys_test.go new file mode 100644 index 00000000..b458162d --- /dev/null +++ b/ocpp1.6/config_manager/keys_test.go @@ -0,0 +1,46 @@ +package ocpp_16_config_manager + +import ( + "testing" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" + "github.com/stretchr/testify/suite" +) + +type keyTestSuite struct { + suite.Suite +} + +func (s *keyTestSuite) TestGetMandatoryKeysForProfile_Core() { + keys := GetMandatoryKeysForProfile(core.ProfileName) + + s.Assert().ElementsMatch(keys, MandatoryCoreKeys) +} + +func (s *keyTestSuite) TestGetMandatoryKeysForProfile_LocalAuth() { + keys := GetMandatoryKeysForProfile(localauth.ProfileName) + + s.Assert().ElementsMatch(keys, MandatoryLocalAuthKeys) +} + +func (s *keyTestSuite) TestGetMandatoryKeysForProfile_Mix() { + keys := GetMandatoryKeysForProfile(core.ProfileName, localauth.ProfileName, firmware.ProfileName, smartcharging.ProfileName) + + expectedKeys := append(MandatoryCoreKeys, MandatoryLocalAuthKeys...) + expectedKeys = append(expectedKeys, MandatoryFirmwareKeys...) + expectedKeys = append(expectedKeys, MandatorySmartChargingKeys...) + + s.Assert().ElementsMatch(keys, expectedKeys) +} + +func (s *keyTestSuite) TestGetMandatoryKeysForProfile_None() { + keys := GetMandatoryKeysForProfile() + s.Assert().Empty(keys) +} + +func TestGetMandatoryKeysForProfile(t *testing.T) { + suite.Run(t, new(keyTestSuite)) +} diff --git a/ocpp1.6/config_manager/manager.go b/ocpp1.6/config_manager/manager.go new file mode 100644 index 00000000..4ad9b4c7 --- /dev/null +++ b/ocpp1.6/config_manager/manager.go @@ -0,0 +1,177 @@ +package ocpp_16_config_manager + +import ( + "errors" + "fmt" + "sync" + + "github.com/agrison/go-commons-lang/stringUtils" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/samber/lo" +) + +var ErrKeyCannotBeEmpty = errors.New("key cannot be empty") + +type ( + KeyValidator func(Key Key, value *string) bool + OnUpdateHandler func(value *string) error + + Manager interface { + SetMandatoryKeys(mandatoryKeys []Key) error + GetMandatoryKeys() []Key + RegisterCustomKeyValidator(KeyValidator) + ValidateKey(key Key, value *string) error + UpdateKey(key Key, value *string) error + OnUpdateKey(key Key, handler OnUpdateHandler) error + GetConfigurationValue(key Key) (*string, error) + SetConfiguration(configuration Config) error + GetConfiguration() ([]core.ConfigurationKey, error) + } + + ManagerV16 struct { + ocppConfig *Config + mandatoryKeys []Key + keyValidator KeyValidator + onUpdateHandlers map[Key]OnUpdateHandler + mu sync.Mutex + } +) + +func NewV16ConfigurationManager(defaultConfiguration Config, profiles ...string) (*ManagerV16, error) { + mandatoryKeys := GetMandatoryKeysForProfile(profiles...) + + // Validate default configuration + err := defaultConfiguration.Validate(mandatoryKeys) + if err != nil { + return nil, err + } + + return &ManagerV16{ + ocppConfig: &defaultConfiguration, + mandatoryKeys: mandatoryKeys, + onUpdateHandlers: make(map[Key]OnUpdateHandler), + mu: sync.Mutex{}, + }, nil +} + +// SetConfiguration validates the provided and overwrites the current configuration +func (m *ManagerV16) SetConfiguration(configuration Config) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Validate the configuration before setting it + err := configuration.Validate(m.mandatoryKeys) + if err != nil { + return err + } + + m.ocppConfig = &configuration + return nil +} + +// RegisterCustomKeyValidator registers a custom key validator +func (m *ManagerV16) RegisterCustomKeyValidator(validator KeyValidator) { + m.keyValidator = validator +} + +// GetMandatoryKeys returns the mandatory keys for the configuration +func (m *ManagerV16) GetMandatoryKeys() []Key { + return m.mandatoryKeys +} + +// SetMandatoryKeys sets the mandatory keys for the configuration +func (m *ManagerV16) SetMandatoryKeys(mandatoryKeys []Key) error { + m.mu.Lock() + defer m.mu.Unlock() + + for _, key := range mandatoryKeys { + isAlreadyPresent := lo.ContainsBy(m.mandatoryKeys, func(k Key) bool { + return k.String() == key.String() + }) + + if isAlreadyPresent { + continue + } + + m.mandatoryKeys = append(m.mandatoryKeys, key) + } + + return nil +} + +// UpdateKey updates the value of a specific key +func (m *ManagerV16) UpdateKey(key Key, value *string) error { + m.mu.Lock() + defer m.mu.Unlock() + + // Validate the key + err := m.ValidateKey(key, value) + if err != nil { + return err + } + + // Try to update the key + err = m.ocppConfig.UpdateKey(key.String(), value) + if err != nil { + return err + } + + // Call the update handler if present + handler, isFound := m.onUpdateHandlers[key] + if isFound { + defer func() { + err = handler(value) + if err != nil { + return + } + }() + } + + return nil +} + +// GetConfiguration returns the full current configuration +func (m *ManagerV16) GetConfiguration() ([]core.ConfigurationKey, error) { + m.mu.Lock() + defer m.mu.Unlock() + + return m.ocppConfig.GetConfig(), nil +} + +// GetConfigurationValue returns the value of a specific key +func (m *ManagerV16) GetConfigurationValue(key Key) (*string, error) { + m.mu.Lock() + defer m.mu.Unlock() + + return m.ocppConfig.GetConfigurationValue(key.String()) +} + +// ValidateKey validates a specific key by checking if there is a custom validator registered +func (m *ManagerV16) ValidateKey(key Key, value *string) error { + if m.keyValidator == nil { + return nil + } + + isValid := m.keyValidator(key, value) + if !isValid { + return fmt.Errorf("key validation failed for key %s", key) + } + + return nil +} + +// OnUpdateKey registers a function to call after a specific key has been updated. +func (m *ManagerV16) OnUpdateKey(key Key, handler OnUpdateHandler) error { + if stringUtils.IsEmpty(key.String()) { + return ErrKeyCannotBeEmpty + } + + // Validate that the key exists + _, err := m.ocppConfig.GetConfigurationValue(key.String()) + if err != nil { + return err + } + + m.onUpdateHandlers[key] = handler + return nil +} diff --git a/ocpp1.6/config_manager/manager_test.go b/ocpp1.6/config_manager/manager_test.go new file mode 100644 index 00000000..f676b80f --- /dev/null +++ b/ocpp1.6/config_manager/manager_test.go @@ -0,0 +1,211 @@ +package ocpp_16_config_manager + +import ( + "strings" + "testing" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging" + "github.com/samber/lo" + "github.com/stretchr/testify/suite" +) + +type ConfigurationManagerTestSuite struct { + suite.Suite + manager *ManagerV16 +} + +func (s *ConfigurationManagerTestSuite) SetupTest() { + profiles := []string{core.ProfileName, smartcharging.ProfileName, localauth.ProfileName} + configuration, err := DefaultConfigurationFromProfiles(profiles...) + s.Require().NoError(err) + + s.manager, err = NewV16ConfigurationManager(*configuration, profiles...) + s.Assert().NoError(err) +} + +func (s *ConfigurationManagerTestSuite) TestNewV16ConfigurationManager() { + configuration, err := DefaultConfigurationFromProfiles(core.ProfileName) + s.Assert().NoError(err) + + // Valid configuration + manager, err := NewV16ConfigurationManager(*configuration, core.ProfileName) + s.Assert().NoError(err) + s.Assert().NotNil(manager) + + // Invalid configuration + manager, err = NewV16ConfigurationManager(NewEmptyConfiguration(), core.ProfileName) + s.Assert().Error(err) + s.Assert().Nil(manager) +} + +func (s *ConfigurationManagerTestSuite) TestGetConfiguration() { + // todo +} + +func (s *ConfigurationManagerTestSuite) TestUpdateConfiguration() { + // Key found + err := s.manager.UpdateKey(HeartbeatInterval, lo.ToPtr("123")) + s.Assert().NoError(err) + + value, err := s.manager.GetConfigurationValue(HeartbeatInterval) + s.Assert().NoError(err) + s.Assert().NotNil(value) + s.Assert().Equal("123", *value) + + err = s.manager.UpdateKey(HeartbeatInterval, nil) + s.Assert().NoError(err) + + value, err = s.manager.GetConfigurationValue(HeartbeatInterval) + s.Assert().NoError(err) + s.Assert().Nil(value) + + // Key not found + err = s.manager.UpdateKey("ExampleKey", lo.ToPtr("exampleValue")) + s.Assert().Error(err) + + err = s.manager.UpdateKey("", lo.ToPtr("exampleValue")) + s.Assert().Error(err) + + err = s.manager.UpdateKey("", nil) + s.Assert().Error(err) +} + +func (s *ConfigurationManagerTestSuite) TestGetConfigurationValue() { + // Tested in the UpdateConfiguration test +} + +func (s *ConfigurationManagerTestSuite) TestOnUpdateKey() { + numExecutions := 0 + + err := s.manager.OnUpdateKey(HeartbeatInterval, func(value *string) error { + numExecutions++ + return nil + }) + _ = s.manager.UpdateKey(HeartbeatInterval, lo.ToPtr("exampleValue")) + _ = s.manager.UpdateKey(HeartbeatInterval, lo.ToPtr("exampleValue")) + _ = s.manager.UpdateKey(HeartbeatInterval, lo.ToPtr("exampleValue")) + s.Assert().NoError(err) + s.Assert().Equal(3, numExecutions) + + err = s.manager.OnUpdateKey("ExampleKey", func(value *string) error { + return nil + }) + s.Assert().Error(err) + + err = s.manager.UpdateKey("ExampleKey", lo.ToPtr("exampleValue")) + s.Assert().Error(err) + + err = s.manager.OnUpdateKey("", func(value *string) error { + numExecutions++ + return nil + }) + s.Assert().Error(err) + + err = s.manager.UpdateKey("", lo.ToPtr("exampleValue")) + s.Assert().Error(err) + s.Assert().Equal(3, numExecutions) +} + +func (s *ConfigurationManagerTestSuite) TestSetConfiguration() { + // todo +} + +func (s *ConfigurationManagerTestSuite) TestValidateKey() { + + s.manager.RegisterCustomKeyValidator(func(key Key, value *string) bool { + switch key { + case HeartbeatInterval: + if value == nil { + return false + } + return true + case LocalAuthListEnabled: + if value == nil { + return false + } + + if strings.ToUpper(*value) == "TRUE" || strings.ToUpper(*value) == "FALSE" { + return true + } + + return false + default: + return false + } + }) + + err := s.manager.ValidateKey(HeartbeatInterval, lo.ToPtr("123")) + s.Assert().NoError(err) + // Should fail - invalid value + err = s.manager.ValidateKey(HeartbeatInterval, nil) + s.Assert().Error(err) + + err = s.manager.ValidateKey(LocalAuthListEnabled, lo.ToPtr("true")) + s.Assert().NoError(err) + + err = s.manager.ValidateKey(LocalAuthListEnabled, lo.ToPtr("false")) + s.Assert().NoError(err) + + // Should fail - invalid value + err = s.manager.ValidateKey(LocalAuthListEnabled, nil) + s.Assert().Error(err) + + err = s.manager.ValidateKey(LocalAuthListEnabled, lo.ToPtr("aaaaa")) + s.Assert().Error(err) + + // Should fail - invalid key + err = s.manager.ValidateKey("ABCD", lo.ToPtr("aaaaa")) + s.Assert().Error(err) + + err = s.manager.ValidateKey("ABCD", nil) + s.Assert().Error(err) +} + +func (s *ConfigurationManagerTestSuite) TestRegisterCustomKeyValidator() { + exampleKey := Key("ExampleKey") + numExecutions := 0 + + s.manager.RegisterCustomKeyValidator(func(key Key, value *string) bool { + numExecutions++ + return exampleKey == key && value != nil && *value == "exampleValue" + }) + + _ = s.manager.UpdateKey(exampleKey, lo.ToPtr("exampleValue")) + s.Assert().Equal(1, numExecutions) +} + +func (s *ConfigurationManagerTestSuite) TestGetMandatoryKeys() { + configuration, err := DefaultConfigurationFromProfiles(core.ProfileName) + s.Assert().NoError(err) + + // Valid configuration + manager, err := NewV16ConfigurationManager(*configuration, core.ProfileName) + s.Assert().NoError(err) + + s.ElementsMatch(MandatoryCoreKeys, manager.GetMandatoryKeys()) + + configuration, err = DefaultConfigurationFromProfiles(core.ProfileName, localauth.ProfileName) + s.Assert().NoError(err) + manager, err = NewV16ConfigurationManager(*configuration, core.ProfileName, localauth.ProfileName) + s.Assert().NoError(err) + + keys := append(MandatoryCoreKeys, MandatoryLocalAuthKeys...) + s.ElementsMatch(keys, manager.GetMandatoryKeys()) +} + +func (s *ConfigurationManagerTestSuite) TestSetMandatoryKeys() { + configuration, err := DefaultConfigurationFromProfiles(core.ProfileName) + s.Assert().NoError(err) + + // Valid configuration + manager, err := NewV16ConfigurationManager(*configuration, core.ProfileName) + s.Assert().NoError(err) + s.Assert().NotNil(manager) + +} + +func TestConfigurationManager(t *testing.T) { + suite.Run(t, new(ConfigurationManagerTestSuite)) +}