diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b60c5a4..49615fa 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -32,7 +32,7 @@ jobs: - name: Install dependencies and run tests run: | go mod download - go test -v ./... -coverpkg=./... -short -coverprofile=unit_coverage.out + go test -v ./... -coverpkg=./... -short -coverprofile=unit_coverage.out -race - name: Archive code coverage results uses: actions/upload-artifact@v4 diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..5ba2d59 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,39 @@ +version: "2" +linters: + default: all + enable: + - bodyclose + - copyloopvar + - decorder + - errcheck + - errname + - forbidigo + - goconst + - gocritic + - gosec + - govet + - ineffassign + - intrange + - misspell + - nestif + - predeclared + - staticcheck + - testifylint + - unparam + - unused + - wastedassign + - whitespace + - wrapcheck + exclusions: + generated: lax + paths: + - ./examples + +formatters: + settings: + goimports: + local-prefixes: + - github.com/ChargePi/ocpp-manager + enable: + - gofmt + - goimports diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b7f7fb5 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +.PHONY:gen format lint + +gen: + mockery + +lint: + golangci-lint run + +format: + golangci-lint fmt \ No newline at end of file diff --git a/README.md b/README.md index 0fa73a9..823058d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ getting and setting values, validating values, and enforcing mandatory keys. - Mandatory key enforcement - Custom value validation - Provides sane default values +- Supports OCPP 1.6 and 2.0.1 (separate packages for each version) ## Roadmap @@ -16,7 +17,7 @@ getting and setting values, validating values, and enforcing mandatory keys. - [x] Custom value validation - [x] Mandatory key enforcement - [x] Support for OCPP 1.6 -- [ ] Support for OCPP 2.0.1 +- [x] Support for OCPP 2.0.1 ## Installing @@ -26,6 +27,8 @@ getting and setting values, validating values, and enforcing mandatory keys. ## ⚡ Usage +### OCPP 1.6 + Check out the full [OCPP 1.6 example](examples/v16/example.go). ```go @@ -92,6 +95,10 @@ func main() { ``` +### OCPP 2.0.1 + +TBD + ## Notes 1. This library is still in development, and the API might change in the future. diff --git a/go.mod b/go.mod index 9330105..5389922 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22.1 replace github.com/lorenzodonini/ocpp-go v0.18.0 => github.com/ChargePi/ocpp-go v0.21.0 require ( - github.com/ChargePi/ocppManager-go v1.2.0 github.com/agrison/go-commons-lang v0.0.0-20240106075236-2e001e6401ef github.com/lorenzodonini/ocpp-go v0.18.0 github.com/samber/lo v1.47.0 diff --git a/go.sum b/go.sum index 8f489fd..7692d64 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/ChargePi/ocpp-go v0.21.0 h1:YP6uCu75D/TJFwkWsRHHtRthapmrxZscKBylBc8oc9Q= github.com/ChargePi/ocpp-go v0.21.0/go.mod h1:2kcukDdhui4u730VfnYVWuwzDLgw+mBRGDir/QAyBhg= -github.com/ChargePi/ocppManager-go v1.2.0 h1:OV90kAD22yVYTSE+uHIgTtiwUOYpEwyHDPJb8d6AltM= -github.com/ChargePi/ocppManager-go v1.2.0/go.mod h1:7kWtV1+qQw+OSOHBuE/wUsTH5Id5+/euzsCfke9NQ2E= 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= diff --git a/ocpp_v201/charging_station.go b/ocpp_v201/charging_station.go new file mode 100644 index 0000000..549c2e1 --- /dev/null +++ b/ocpp_v201/charging_station.go @@ -0,0 +1,9 @@ +package ocpp_v201 + +import "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + +type ChargingStation struct { + ID string + components map[component.ComponentName]component.Component // Charging station (top level) specific components + controllerManager Manager +} diff --git a/ocpp_v201/component/component.go b/ocpp_v201/component/component.go new file mode 100644 index 0000000..0ef6199 --- /dev/null +++ b/ocpp_v201/component/component.go @@ -0,0 +1,62 @@ +package component + +import "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + +type Component interface { + + // GetName Essentially a component type. + GetName() ComponentName + + // GetInstanceId returns the unique instance ID of this component. + GetInstanceId() string + + // RegisterSubComponent registers a sub-component to this component. + RegisterSubComponent(component Component) + + // UnregisterSubComponent unregisters a sub-component from this component. + UnregisterSubComponent(component Component) + + // GetSubComponents returns all sub-components of this component. + GetSubComponents() []Component + + // GetRequiredVariables returns required variables for this component + GetRequiredVariables() []variables.VariableName + + // GetSupportedVariables returns supported variables (both required and optional) for this component + GetSupportedVariables() []variables.VariableName + + // GetVariable retrieves a variable by its name. + GetVariable(key variables.VariableName, opts ...GetSetVariableOption) (*variables.Variable, error) + + // UpdateVariable updates a variable's attribute with a new value. + UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...GetSetVariableOption) error + + // Validate checks if the variable is valid for this component. + Validate(key variables.VariableName) bool +} + +type ComponentName string + +const ( + ComponentNameOCPPCommCtrlr ComponentName = "OCPPCommCtrlr" + ComponentNameLocalAuthListCtrlr ComponentName = "LocalAuthListCtrlr" + ComponentNameTxCtrlr ComponentName = "TxCtrlr" + ComponentNameDeviceDataCtrlr ComponentName = "DeviceDataCtrlr" + ComponentNameSecurityCtrlr ComponentName = "SecurityCtrlr" + ComponentNameClockCtrlr ComponentName = "ClockCtrlr" + ComponentNameCustomizationCtrlr ComponentName = "CustomizationCtrlr" + ComponentNameSampledDataCtrlr ComponentName = "SampledDataCtrlr" + ComponentNameAlignedDataCtrlr ComponentName = "AlignedDataCtrlr" + ComponentNameReservationCtrlr ComponentName = "ReservationCtrlr" + ComponentNameSmartChargingCtrlr ComponentName = "SmartChargingCtrlr" + ComponentNameTariffCostCtrlr ComponentName = "TariffCostCtrlr" + ComponentNameMonitoringCtrlr ComponentName = "MonitoringCtrlr" + ComponentNameDisplayMessageCtrlr ComponentName = "DisplayMessageCtrlr" + ComponentNameISO15118Ctrlr ComponentName = "ISO15118Ctrlr" + ComponentNameAuthCtrlr ComponentName = "AuthCtrlr" + ComponentNameAuthCacheCtrlr ComponentName = "AuthCacheCtrlr" + ComponentNameChargingStation ComponentName = "ChargingStation" + ComponentNameEVSE ComponentName = "EVSE" + ComponentNameConnector ComponentName = "Connector" + ComponentNameConnectedEV ComponentName = "ConnectedEV" +) diff --git a/ocpp_v201/component/component_variable_opts.go b/ocpp_v201/component/component_variable_opts.go new file mode 100644 index 0000000..70f17e3 --- /dev/null +++ b/ocpp_v201/component/component_variable_opts.go @@ -0,0 +1,14 @@ +package component + +type GetSetVariableOption func(o *componentVariableOptions) + +type componentVariableOptions struct { + attributeType string +} + +// WithAttributeType sets the attribute type for the variable options. +func WithAttributeType(attributeType string) GetSetVariableOption { + return func(o *componentVariableOptions) { + o.attributeType = attributeType + } +} diff --git a/ocpp_v201/component/component_variable_opts_test.go b/ocpp_v201/component/component_variable_opts_test.go new file mode 100644 index 0000000..38a2b5c --- /dev/null +++ b/ocpp_v201/component/component_variable_opts_test.go @@ -0,0 +1,42 @@ +package component + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestOptions(t *testing.T) { + tests := []struct { + name string + opts []GetSetVariableOption + expected componentVariableOptions + }{ + { + name: "default options", + expected: componentVariableOptions{ + attributeType: "", + }, + opts: []GetSetVariableOption{}, + }, + { + name: "with attribute type", + expected: componentVariableOptions{ + attributeType: "abc", + }, + opts: []GetSetVariableOption{ + WithAttributeType("abc"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := componentVariableOptions{} + for _, opt := range tt.opts { + opt(&opts) + } + + assert.Equal(t, tt.expected, opts) + }) + } +} diff --git a/ocpp_v201/connector.go b/ocpp_v201/connector.go new file mode 100644 index 0000000..fc92951 --- /dev/null +++ b/ocpp_v201/connector.go @@ -0,0 +1,8 @@ +package ocpp_v201 + +import "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + +type Connector struct { + ID int + components map[component.ComponentName]component.Component +} diff --git a/ocpp_v201/controllers/aligned_data_ctrlr.go b/ocpp_v201/controllers/aligned_data_ctrlr.go new file mode 100644 index 0000000..5ed9dc0 --- /dev/null +++ b/ocpp_v201/controllers/aligned_data_ctrlr.go @@ -0,0 +1,147 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameAlignedDataEnabled variables.VariableName = "Enabled" + VariableNameAlignedDataAvailable variables.VariableName = "Available" + VariableNameAlignedDataMeasurands variables.VariableName = "Measurands" + VariableNameAlignedDataInterval variables.VariableName = "Interval" + VariableNameAlignedDataSendDuringIdle variables.VariableName = "SendDuringIdle" + VariableNameAlignedDataSignReadings variables.VariableName = "SignReadings" + VariableNameAlignedDataTxEndedMeasurands variables.VariableName = "TxEndedMeasurands" + VariableNameAlignedDataTxEndedInterval variables.VariableName = "TxEndedInterval" +) + +func requiredAlignedDataVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameAlignedDataMeasurands, + VariableNameAlignedDataInterval, + VariableNameAlignedDataTxEndedMeasurands, + VariableNameAlignedDataTxEndedInterval, + } +} + +func optionalAlignedDataVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameAlignedDataEnabled, + VariableNameAlignedDataAvailable, + VariableNameAlignedDataSendDuringIdle, + VariableNameAlignedDataSignReadings, + } +} + +func supportedAlignedDataVariables() []variables.VariableName { + return append(requiredAlignedDataVariables(), optionalAlignedDataVariables()...) +} + +type AlignedDataCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (a *AlignedDataCtrlr) GetName() component.ComponentName { + return component.ComponentNameAlignedDataCtrlr +} + +func (a *AlignedDataCtrlr) GetInstanceId() string { + return a.instanceId +} + +func (a *AlignedDataCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (a *AlignedDataCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (a *AlignedDataCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (a *AlignedDataCtrlr) GetRequiredVariables() []variables.VariableName { + a.mu.RLock() + defer a.mu.RUnlock() + + return a.requiredVariables +} + +func (a *AlignedDataCtrlr) GetSupportedVariables() []variables.VariableName { + a.mu.RLock() + defer a.mu.RUnlock() + + return a.supportedVariables +} + +func (a *AlignedDataCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !a.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + a.mu.RLock() + defer a.mu.RUnlock() + + variable, exists := a.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (a *AlignedDataCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !a.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + a.mu.Lock() + defer a.mu.Unlock() + + v, exists := a.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (a *AlignedDataCtrlr) Validate(key variables.VariableName) bool { + if !a.validator.IsVariableSupported(key) { + return false + } + + a.mu.RLock() + defer a.mu.RUnlock() + + v, exists := a.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewAlignedDataCtrlr() *AlignedDataCtrlr { + ctrlr := &AlignedDataCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredAlignedDataVariables(), + supportedVariables: supportedAlignedDataVariables(), + instanceId: "aligned-data-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + return ctrlr +} diff --git a/ocpp_v201/controllers/aligned_data_ctrlr_test.go b/ocpp_v201/controllers/aligned_data_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/aligned_data_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/auth_cache_ctrlr.go b/ocpp_v201/controllers/auth_cache_ctrlr.go new file mode 100644 index 0000000..5ff0fe6 --- /dev/null +++ b/ocpp_v201/controllers/auth_cache_ctrlr.go @@ -0,0 +1,141 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameAuthCacheEnabled variables.VariableName = "Enabled" + VariableNameAuthCacheAvailable variables.VariableName = "Available" + VariableNameAuthCacheLifeTime variables.VariableName = "LifeTime" + VariableNameAuthCacheStorage variables.VariableName = "Storage" + VariableNameAuthCachePolicy variables.VariableName = "Policy" + VariableNameAuthCacheDisablePostAuthorize variables.VariableName = "DisablePostAuthorize" +) + +func requiredAuthCacheVariables() []variables.VariableName { + return []variables.VariableName{} +} + +func optionalAuthCacheVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameAuthCacheAvailable, + VariableNameAuthCacheEnabled, + VariableNameAuthCacheLifeTime, + VariableNameAuthCacheStorage, + VariableNameAuthCachePolicy, + VariableNameAuthCacheDisablePostAuthorize, + } +} + +func supportedAuthCacheVariables() []variables.VariableName { + return append(requiredAuthCacheVariables(), optionalAuthCacheVariables()...) +} + +type AuthCacheCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + supportedVariables []variables.VariableName + requiredVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (a *AuthCacheCtrlr) GetName() component.ComponentName { + return component.ComponentNameAuthCacheCtrlr +} + +func (a *AuthCacheCtrlr) GetInstanceId() string { + return a.instanceId +} + +func (a *AuthCacheCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (a *AuthCacheCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (a *AuthCacheCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (a *AuthCacheCtrlr) GetRequiredVariables() []variables.VariableName { + a.mu.RLock() + defer a.mu.RUnlock() + + return a.requiredVariables +} + +func (a *AuthCacheCtrlr) GetSupportedVariables() []variables.VariableName { + a.mu.RLock() + defer a.mu.RUnlock() + + return a.supportedVariables +} + +func (a *AuthCacheCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !a.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + a.mu.RLock() + defer a.mu.RUnlock() + + variable, exists := a.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (a *AuthCacheCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !a.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + a.mu.Lock() + defer a.mu.Unlock() + + v, exists := a.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (a *AuthCacheCtrlr) Validate(key variables.VariableName) bool { + if !a.validator.IsVariableSupported(key) { + return false + } + + a.mu.RLock() + defer a.mu.RUnlock() + + v, exists := a.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewAuthCacheCtrlr() *AuthCacheCtrlr { + ctrlr := &AuthCacheCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + supportedVariables: supportedAuthCacheVariables(), + requiredVariables: requiredAuthCacheVariables(), + instanceId: "auth-cache-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + return ctrlr +} diff --git a/ocpp_v201/controllers/auth_cache_ctrlr_test.go b/ocpp_v201/controllers/auth_cache_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/auth_cache_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/auth_ctrlr.go b/ocpp_v201/controllers/auth_ctrlr.go new file mode 100644 index 0000000..676750c --- /dev/null +++ b/ocpp_v201/controllers/auth_ctrlr.go @@ -0,0 +1,142 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameAuthEnabled variables.VariableName = "Enabled" + VariableNameAdditionalInfoItemsPerMessage variables.VariableName = "AdditionalInfoItemsPerMessage" + VariableNameOfflineTxForUnknownIdEnabled variables.VariableName = "OfflineTxForUnknownIdEnabled" + VariableNameAuthorizeRemoteStart variables.VariableName = "AuthorizeRemoteStart" + VariableNameLocalAuthorizeOffline variables.VariableName = "LocalAuthorizeOffline" + VariableNameLocalPreAuthorize variables.VariableName = "LocalPreAuthorize" + VariableNameMasterPassGroupId variables.VariableName = "MasterPassGroupId" + VariableNameDisableRemoteAuthorization variables.VariableName = "DisableRemoteAuthorization" +) + +func requiredAuthVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameAuthorizeRemoteStart, + VariableNameLocalAuthorizeOffline, + VariableNameLocalPreAuthorize, + } +} + +func optionalAuthVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameAuthEnabled, + VariableNameAdditionalInfoItemsPerMessage, + VariableNameOfflineTxForUnknownIdEnabled, + VariableNameMasterPassGroupId, + VariableNameDisableRemoteAuthorization, + } +} + +func supportedAuthVariables() []variables.VariableName { + return append(requiredAuthVariables(), optionalAuthVariables()...) +} + +type AuthCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (a *AuthCtrlr) GetName() component.ComponentName { + return component.ComponentNameAuthCtrlr +} + +func (a *AuthCtrlr) GetInstanceId() string { + return a.instanceId +} + +func (a *AuthCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (a *AuthCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (a *AuthCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (a *AuthCtrlr) GetRequiredVariables() []variables.VariableName { + return a.requiredVariables +} + +func (a *AuthCtrlr) GetSupportedVariables() []variables.VariableName { + return a.supportedVariables +} + +func (a *AuthCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !a.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + a.mu.RLock() + defer a.mu.RUnlock() + + variable, exists := a.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (a *AuthCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !a.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + a.mu.Lock() + defer a.mu.Unlock() + + v, exists := a.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (a *AuthCtrlr) Validate(key variables.VariableName) bool { + if !a.validator.IsVariableSupported(key) { + return false + } + + a.mu.RLock() + defer a.mu.RUnlock() + + v, exists := a.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewAuthCtrlr() *AuthCtrlr { + ctrlr := &AuthCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredAuthVariables(), + supportedVariables: supportedAuthVariables(), + instanceId: "auth-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/auth_ctrlr_test.go b/ocpp_v201/controllers/auth_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/auth_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/chademo_ctrlr.go b/ocpp_v201/controllers/chademo_ctrlr.go new file mode 100644 index 0000000..90545bf --- /dev/null +++ b/ocpp_v201/controllers/chademo_ctrlr.go @@ -0,0 +1,13 @@ +package controllers + +import variables2 "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + +type ChademoCtrlr struct { + variables map[variables2.VariableName]variables2.Variable + requiredVariables []variables2.VariableName + supportedVariables []variables2.VariableName +} + +func NewChademoCtrlr() *ChademoCtrlr { + return &ChademoCtrlr{} +} diff --git a/ocpp_v201/controllers/chademo_ctrlr_test.go b/ocpp_v201/controllers/chademo_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/chademo_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/clock_ctrlr.go b/ocpp_v201/controllers/clock_ctrlr.go new file mode 100644 index 0000000..4b39571 --- /dev/null +++ b/ocpp_v201/controllers/clock_ctrlr.go @@ -0,0 +1,150 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameDateTime variables.VariableName = "DateTime" + VariableNameNtpSource variables.VariableName = "NtpSource" + VariableNameNtpServerUri variables.VariableName = "NtpServerUri" + VariableNameTimeOffset variables.VariableName = "TimeOffset" + VariableNameNextTimeOffsetTransitionDateTime variables.VariableName = "NextTimeOffsetTransitionDateTime" + VariableNameTimeOffsetNextTransition variables.VariableName = "TimeOffsetNextTransition" + VariableNameTimeSource variables.VariableName = "TimeSource" + VariableNameTimeZone variables.VariableName = "TimeZone" + VariableNameTimeAdjustmentReportingThreshold variables.VariableName = "TimeAdjustmentReportingThreshold" +) + +func requiredClockVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameDateTime, + VariableNameTimeSource, + } +} + +func optionalClockVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameTimeZone, + VariableNameNtpSource, + VariableNameNtpServerUri, + VariableNameTimeOffsetNextTransition, + VariableNameNextTimeOffsetTransitionDateTime, + VariableNameTimeOffset, + VariableNameTimeAdjustmentReportingThreshold, + } +} + +// supportedClockVariables returns a list of all variables supported by the ClockCtrlr. +func supportedClockVariables() []variables.VariableName { + return append(requiredClockVariables(), optionalClockVariables()...) +} + +type ClockCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (c *ClockCtrlr) GetName() component.ComponentName { + return component.ComponentNameClockCtrlr +} + +func (c *ClockCtrlr) GetInstanceId() string { + return c.instanceId +} + +func (c *ClockCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (c *ClockCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (c *ClockCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (c *ClockCtrlr) GetRequiredVariables() []variables.VariableName { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.requiredVariables +} + +func (c *ClockCtrlr) GetSupportedVariables() []variables.VariableName { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.supportedVariables +} + +func (c *ClockCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !c.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + c.mu.RLock() + defer c.mu.RUnlock() + + variable, exists := c.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (c *ClockCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !c.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + c.mu.Lock() + defer c.mu.Unlock() + + v, exists := c.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (c *ClockCtrlr) Validate(key variables.VariableName) bool { + if !c.validator.IsVariableSupported(key) { + return false + } + + c.mu.RLock() + defer c.mu.RUnlock() + + v, exists := c.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewClockCtrlr() *ClockCtrlr { + ctrlr := &ClockCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredClockVariables(), + supportedVariables: supportedClockVariables(), + instanceId: "clock-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/clock_ctrlr_test.go b/ocpp_v201/controllers/clock_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/clock_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/customization_ctrlr.go b/ocpp_v201/controllers/customization_ctrlr.go new file mode 100644 index 0000000..c9a0877 --- /dev/null +++ b/ocpp_v201/controllers/customization_ctrlr.go @@ -0,0 +1,141 @@ +package controllers + +import ( + "fmt" + "slices" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameCustomImplementationEnabled variables.VariableName = "CustomImplementationEnabled" +) + +type CustomizationCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]variables.Variable + requiredVariables []variables.VariableName // Set + supportedVariables []variables.VariableName // Set + instanceId string + validator *variableValidator +} + +func (c *CustomizationCtrlr) GetName() component.ComponentName { + return component.ComponentNameCustomizationCtrlr +} + +func (c *CustomizationCtrlr) GetInstanceId() string { + return c.instanceId +} + +func (c *CustomizationCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (c *CustomizationCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (c *CustomizationCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (c *CustomizationCtrlr) GetRequiredVariables() []variables.VariableName { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.requiredVariables +} + +func (c *CustomizationCtrlr) GetSupportedVariables() []variables.VariableName { + c.mu.RLock() + defer c.mu.RUnlock() + + return c.supportedVariables +} + +func (c *CustomizationCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !c.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + c.mu.RLock() + defer c.mu.RUnlock() + + variable, exists := c.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return &variable, nil +} + +func (c *CustomizationCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !c.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + c.mu.Lock() + defer c.mu.Unlock() + + v, exists := c.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (c *CustomizationCtrlr) Validate(key variables.VariableName) bool { + if !c.validator.IsVariableSupported(key) { + return false + } + + c.mu.RLock() + defer c.mu.RUnlock() + + v, exists := c.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func (c *CustomizationCtrlr) AddRequiredVariable(key variables.VariableName) { + c.mu.Lock() + defer c.mu.Unlock() + + if slices.Contains(c.requiredVariables, key) || slices.Contains(c.supportedVariables, key) { + return + } + + c.requiredVariables = append(c.requiredVariables, key) +} + +func (c *CustomizationCtrlr) AddSupportedVariable(key variables.VariableName) { + c.mu.Lock() + defer c.mu.Unlock() + + if slices.Contains(c.supportedVariables, key) || slices.Contains(c.requiredVariables, key) { + return + } + + c.supportedVariables = append(c.supportedVariables, key) +} + +func NewCustomizationCtrlr() *CustomizationCtrlr { + ctrlr := &CustomizationCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]variables.Variable), + requiredVariables: make([]variables.VariableName, 0), + supportedVariables: make([]variables.VariableName, 0), + instanceId: "customization-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/customization_ctrlr_test.go b/ocpp_v201/controllers/customization_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/customization_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/device_data_ctrlr.go b/ocpp_v201/controllers/device_data_ctrlr.go new file mode 100644 index 0000000..b42ab55 --- /dev/null +++ b/ocpp_v201/controllers/device_data_ctrlr.go @@ -0,0 +1,178 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameBytesPerMessage variables.VariableName = "BytesPerMessage" + VariableNameConfigurationValueSize variables.VariableName = "ConfigurationValueSize" + VariableNameReportingValueSize variables.VariableName = "ReportingValueSize" + VariableNameItemsPerMessage variables.VariableName = "ItemsPerMessage" +) + +func requiredVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameBytesPerMessage, + VariableNameConfigurationValueSize, + VariableNameReportingValueSize, + VariableNameItemsPerMessage, + } +} + +func optionalVariables() []variables.VariableName { + return []variables.VariableName{} +} + +func supportedVariables() []variables.VariableName { + return append(requiredVariables(), optionalVariables()...) +} + +type DeviceDataCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (d *DeviceDataCtrlr) GetName() component.ComponentName { + return component.ComponentNameDeviceDataCtrlr +} + +func (d *DeviceDataCtrlr) GetInstanceId() string { + return d.instanceId +} + +func (d *DeviceDataCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (d *DeviceDataCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (d *DeviceDataCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (d *DeviceDataCtrlr) GetRequiredVariables() []variables.VariableName { + d.mu.RLock() + defer d.mu.RUnlock() + + return d.requiredVariables +} + +func (d *DeviceDataCtrlr) GetSupportedVariables() []variables.VariableName { + d.mu.RLock() + defer d.mu.RUnlock() + + return d.supportedVariables +} + +func (d *DeviceDataCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !d.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + d.mu.Lock() + defer d.mu.Unlock() + + variable, exists := d.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (d *DeviceDataCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !d.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + d.mu.RLock() + defer d.mu.RUnlock() + + v, exists := d.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (d *DeviceDataCtrlr) Validate(key variables.VariableName) bool { + if !d.validator.IsVariableSupported(key) { + return false + } + + d.mu.RLock() + defer d.mu.RUnlock() + + // Check if the variable exists + v, exists := d.variables[key] + if !exists { + return false + } + + // Validate the variable itself + return v.Validate() +} + +// ValidateAllRequiredVariables checks that all required variables are present and valid +func (d *DeviceDataCtrlr) ValidateAllRequiredVariables() bool { + d.mu.RLock() + defer d.mu.RUnlock() + + for _, requiredVar := range d.requiredVariables { + // Check if the variable exists + v, exists := d.variables[requiredVar] + if !exists { + return false + } + // Validate the variable itself + if !v.Validate() { + return false + } + } + + return true +} + +func NewDeviceDataCtrlr() *DeviceDataCtrlr { + ctrlr := &DeviceDataCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredVariables(), + supportedVariables: supportedVariables(), + instanceId: "device-data-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + // Initialize all required variables with default values + ctrlr.variables[VariableNameBytesPerMessage] = variables.NewVariable( + VariableNameBytesPerMessage, + variables.VariableTypeInteger, + ) + ctrlr.variables[VariableNameConfigurationValueSize] = variables.NewVariable( + VariableNameConfigurationValueSize, + variables.VariableTypeInteger, + ) + ctrlr.variables[VariableNameReportingValueSize] = variables.NewVariable( + VariableNameReportingValueSize, + variables.VariableTypeInteger, + ) + ctrlr.variables[VariableNameItemsPerMessage] = variables.NewVariable( + VariableNameItemsPerMessage, + variables.VariableTypeInteger, + ) + + return ctrlr +} diff --git a/ocpp_v201/controllers/device_data_ctrlr_test.go b/ocpp_v201/controllers/device_data_ctrlr_test.go new file mode 100644 index 0000000..7aa6d80 --- /dev/null +++ b/ocpp_v201/controllers/device_data_ctrlr_test.go @@ -0,0 +1,266 @@ +package controllers + +import ( + "testing" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + "github.com/stretchr/testify/suite" +) + +type DeviceDataCtrlrTestSuite struct { + suite.Suite + ctrlr *DeviceDataCtrlr +} + +func (suite *DeviceDataCtrlrTestSuite) SetupTest() { + suite.ctrlr = NewDeviceDataCtrlr() +} + +func (suite *DeviceDataCtrlrTestSuite) TestNewDeviceDataCtrlr() { + ctrlr := NewDeviceDataCtrlr() + + suite.NotNil(ctrlr) + suite.Equal(component.ComponentNameDeviceDataCtrlr, ctrlr.GetName()) + suite.Equal("device-data-ctrlr", ctrlr.GetInstanceId()) + suite.NotNil(ctrlr.validator) + suite.NotEmpty(ctrlr.GetRequiredVariables()) + suite.NotEmpty(ctrlr.GetSupportedVariables()) +} + +func (suite *DeviceDataCtrlrTestSuite) TestGetName() { + suite.Equal(component.ComponentNameDeviceDataCtrlr, suite.ctrlr.GetName()) +} + +func (suite *DeviceDataCtrlrTestSuite) TestGetInstanceId() { + suite.Equal("device-data-ctrlr", suite.ctrlr.GetInstanceId()) +} + +func (suite *DeviceDataCtrlrTestSuite) TestSubComponentMethods() { + // Test that sub-component methods are no-op + initialCount := len(suite.ctrlr.GetSubComponents()) + + // Register should be no-op + suite.ctrlr.RegisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "RegisterSubComponent should be a no-op") + + // Unregister should be no-op + suite.ctrlr.UnregisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "UnregisterSubComponent should be a no-op") +} + +func (suite *DeviceDataCtrlrTestSuite) TestGetSubComponents() { + subComponents := suite.ctrlr.GetSubComponents() + suite.Equal(0, len(subComponents)) +} + +func (suite *DeviceDataCtrlrTestSuite) TestGetRequiredVariables() { + requiredVars := suite.ctrlr.GetRequiredVariables() + suite.NotEmpty(requiredVars) + + expectedVars := []variables.VariableName{ + VariableNameBytesPerMessage, + VariableNameConfigurationValueSize, + VariableNameReportingValueSize, + } + + for _, expectedVar := range expectedVars { + suite.Contains(requiredVars, expectedVar) + } +} + +func (suite *DeviceDataCtrlrTestSuite) TestGetSupportedVariables() { + supportedVars := suite.ctrlr.GetSupportedVariables() + suite.NotEmpty(supportedVars) + + // Should include all required variables + requiredVars := suite.ctrlr.GetRequiredVariables() + for _, requiredVar := range requiredVars { + suite.Contains(supportedVars, requiredVar) + } +} + +func (suite *DeviceDataCtrlrTestSuite) TestGetVariable() { + tests := []struct { + name string + variableName variables.VariableName + setupVariable bool + expectError bool + errorContains string + }{ + { + name: "existing variable", + variableName: VariableNameBytesPerMessage, + setupVariable: true, + expectError: false, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + setupVariable: false, + expectError: true, + errorContains: "not found", + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + setupVariable: false, + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + if tt.setupVariable { + // Variable is already set up in constructor + } + + result, err := suite.ctrlr.GetVariable(tt.variableName) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + suite.Nil(result) + } else { + suite.NoError(err) + suite.NotNil(result) + suite.Equal(tt.variableName, result.Name) + } + }) + } +} + +func (suite *DeviceDataCtrlrTestSuite) TestUpdateVariable() { + tests := []struct { + name string + variableName variables.VariableName + attribute string + value interface{} + setupVariable bool + expectError bool + errorContains string + }{ + { + name: "update existing variable", + variableName: VariableNameBytesPerMessage, + attribute: "value", + value: int64(1024), + setupVariable: true, + expectError: false, + }, + { + name: "update non-existent variable", + variableName: "NonExistentVariable", + attribute: "value", + value: int64(1024), + setupVariable: false, + expectError: true, + errorContains: "not found", + }, + { + name: "update unsupported variable", + variableName: "UnsupportedVariable", + attribute: "value", + value: int64(1024), + setupVariable: false, + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + err := suite.ctrlr.UpdateVariable(tt.variableName, tt.attribute, tt.value) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + } else { + suite.NoError(err) + + // Verify the update worked + variable, err := suite.ctrlr.GetVariable(tt.variableName) + suite.NoError(err) + suite.NotNil(variable) + } + }) + } +} + +func (suite *DeviceDataCtrlrTestSuite) TestValidate() { + tests := []struct { + name string + variableName variables.VariableName + setupVariable bool + expected bool + }{ + { + name: "valid existing variable", + variableName: VariableNameBytesPerMessage, + setupVariable: true, + expected: true, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + setupVariable: false, + expected: false, + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + setupVariable: false, + expected: false, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result := suite.ctrlr.Validate(tt.variableName) + suite.Equal(tt.expected, result) + }) + } +} + +func (suite *DeviceDataCtrlrTestSuite) TestValidateAllRequiredVariables() { + // Test with all required variables present (default state) + result := suite.ctrlr.ValidateAllRequiredVariables() + suite.True(result) +} + +func (suite *DeviceDataCtrlrTestSuite) TestThreadSafety() { + // Test concurrent access to the controller + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func() { + // Concurrent reads + _, _ = suite.ctrlr.GetVariable(VariableNameBytesPerMessage) + _ = suite.ctrlr.Validate(VariableNameBytesPerMessage) + _ = suite.ctrlr.GetRequiredVariables() + _ = suite.ctrlr.GetSupportedVariables() + + // Concurrent writes + _ = suite.ctrlr.UpdateVariable(VariableNameBytesPerMessage, "value", int64(512)) + + done <- true + }() + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Verify the controller is still in a valid state + suite.True(suite.ctrlr.ValidateAllRequiredVariables()) +} + +func TestDeviceDataCtrlrTestSuite(t *testing.T) { + suite.Run(t, new(DeviceDataCtrlrTestSuite)) +} diff --git a/ocpp_v201/controllers/display_message_ctrlr.go b/ocpp_v201/controllers/display_message_ctrlr.go new file mode 100644 index 0000000..c58159f --- /dev/null +++ b/ocpp_v201/controllers/display_message_ctrlr.go @@ -0,0 +1,142 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameDisplayMessageEnabled variables.VariableName = "Enabled" + VariableNameDisplayMessageAvailable variables.VariableName = "Available" + VariableNameNumberOfDisplayMessages variables.VariableName = "DisplayMessages" + VariableNameDisplayMessageSupportedFormats variables.VariableName = "SupportedFormats" + VariableNameDisplayMessageSupportedPriorities variables.VariableName = "SupportedPriorities" +) + +func requiredDisplayMessageVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameNumberOfDisplayMessages, + VariableNameDisplayMessageSupportedFormats, + VariableNameDisplayMessageSupportedPriorities, + } +} + +func optionalDisplayMessageVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameDisplayMessageEnabled, + VariableNameDisplayMessageAvailable, + } +} + +func supportedDisplayMessageVariables() []variables.VariableName { + return append(requiredDisplayMessageVariables(), optionalDisplayMessageVariables()...) +} + +type DisplayCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (d *DisplayCtrlr) GetName() component.ComponentName { + return component.ComponentNameDisplayMessageCtrlr +} + +func (d *DisplayCtrlr) GetInstanceId() string { + return d.instanceId +} + +func (d *DisplayCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (d *DisplayCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (d *DisplayCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (d *DisplayCtrlr) GetRequiredVariables() []variables.VariableName { + d.mu.RLock() + defer d.mu.RUnlock() + + return d.requiredVariables +} + +func (d *DisplayCtrlr) GetSupportedVariables() []variables.VariableName { + d.mu.RLock() + defer d.mu.RUnlock() + + return d.supportedVariables +} + +func (d *DisplayCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !d.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + d.mu.RLock() + defer d.mu.RUnlock() + + variable, exists := d.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (d *DisplayCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !d.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + d.mu.Lock() + defer d.mu.Unlock() + + v, exists := d.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (d *DisplayCtrlr) Validate(key variables.VariableName) bool { + if !d.validator.IsVariableSupported(key) { + return false + } + + d.mu.RLock() + defer d.mu.RUnlock() + + v, exists := d.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewDisplayCtrlr() *DisplayCtrlr { + ctrlr := &DisplayCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredDisplayMessageVariables(), + supportedVariables: supportedDisplayMessageVariables(), + instanceId: "display-message-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/display_message_ctrlr_test.go b/ocpp_v201/controllers/display_message_ctrlr_test.go new file mode 100644 index 0000000..df66fc0 --- /dev/null +++ b/ocpp_v201/controllers/display_message_ctrlr_test.go @@ -0,0 +1,276 @@ +package controllers + +import ( + "testing" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + "github.com/stretchr/testify/suite" +) + +type DisplayCtrlrTestSuite struct { + suite.Suite + ctrlr *DisplayCtrlr +} + +func (suite *DisplayCtrlrTestSuite) SetupTest() { + suite.ctrlr = NewDisplayCtrlr() +} + +func (suite *DisplayCtrlrTestSuite) TestNewDisplayCtrlr() { + ctrlr := NewDisplayCtrlr() + + suite.NotNil(ctrlr) + suite.Equal(component.ComponentNameDisplayMessageCtrlr, ctrlr.GetName()) + suite.Equal("display-message-ctrlr", ctrlr.GetInstanceId()) + suite.NotNil(ctrlr.validator) + suite.NotEmpty(ctrlr.GetRequiredVariables()) + suite.NotEmpty(ctrlr.GetSupportedVariables()) +} + +func (suite *DisplayCtrlrTestSuite) TestGetName() { + suite.Equal(component.ComponentNameDisplayMessageCtrlr, suite.ctrlr.GetName()) +} + +func (suite *DisplayCtrlrTestSuite) TestGetInstanceId() { + suite.Equal("display-message-ctrlr", suite.ctrlr.GetInstanceId()) +} + +func (suite *DisplayCtrlrTestSuite) TestSubComponentMethods() { + // Test that sub-component methods are no-op + initialCount := len(suite.ctrlr.GetSubComponents()) + + // Register should be no-op + suite.ctrlr.RegisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "RegisterSubComponent should be a no-op") + + // Unregister should be no-op + suite.ctrlr.UnregisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "UnregisterSubComponent should be a no-op") +} + +func (suite *DisplayCtrlrTestSuite) TestGetSubComponents() { + subComponents := suite.ctrlr.GetSubComponents() + suite.Equal(0, len(subComponents)) +} + +func (suite *DisplayCtrlrTestSuite) TestGetRequiredVariables() { + requiredVars := suite.ctrlr.GetRequiredVariables() + suite.NotEmpty(requiredVars) + + expectedVars := []variables.VariableName{ + VariableNameNumberOfDisplayMessages, + VariableNameDisplayMessageSupportedFormats, + VariableNameDisplayMessageSupportedPriorities, + } + + for _, expectedVar := range expectedVars { + suite.Contains(requiredVars, expectedVar) + } +} + +func (suite *DisplayCtrlrTestSuite) TestGetSupportedVariables() { + supportedVars := suite.ctrlr.GetSupportedVariables() + suite.NotEmpty(supportedVars) + + // Should include all required variables + requiredVars := suite.ctrlr.GetRequiredVariables() + for _, requiredVar := range requiredVars { + suite.Contains(supportedVars, requiredVar) + } +} + +func (suite *DisplayCtrlrTestSuite) TestGetVariable() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameDisplayMessageSupportedFormats, + variables.VariableTypeString, + "ASCII", + ) + suite.ctrlr.variables[VariableNameDisplayMessageSupportedFormats] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + expectError bool + errorContains string + }{ + { + name: "existing variable", + variableName: VariableNameDisplayMessageSupportedFormats, + expectError: false, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + expectError: true, + errorContains: "not found", + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result, err := suite.ctrlr.GetVariable(tt.variableName) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + suite.Nil(result) + } else { + suite.NoError(err) + suite.NotNil(result) + suite.Equal(tt.variableName, result.Name) + } + }) + } +} + +func (suite *DisplayCtrlrTestSuite) TestUpdateVariable() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameDisplayMessageSupportedFormats, + variables.VariableTypeString, + "ASCII", + ) + suite.ctrlr.variables[VariableNameDisplayMessageSupportedFormats] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + attribute string + value interface{} + expectError bool + errorContains string + }{ + { + name: "update existing variable", + variableName: VariableNameDisplayMessageSupportedFormats, + attribute: "value", + value: "UTF8", + expectError: false, + }, + { + name: "update non-existent variable", + variableName: "NonExistentVariable", + attribute: "value", + value: "UTF8", + expectError: true, + errorContains: "not found", + }, + { + name: "update unsupported variable", + variableName: "UnsupportedVariable", + attribute: "value", + value: "UTF8", + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + err := suite.ctrlr.UpdateVariable(tt.variableName, tt.attribute, tt.value) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + } else { + suite.NoError(err) + + // Verify the update worked + variable, err := suite.ctrlr.GetVariable(tt.variableName) + suite.NoError(err) + suite.NotNil(variable) + } + }) + } +} + +func (suite *DisplayCtrlrTestSuite) TestValidate() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameDisplayMessageSupportedFormats, + variables.VariableTypeString, + "ASCII", + ) + suite.ctrlr.variables[VariableNameDisplayMessageSupportedFormats] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + expected bool + }{ + { + name: "valid existing variable", + variableName: VariableNameDisplayMessageSupportedFormats, + expected: true, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + expected: false, + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + expected: false, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result := suite.ctrlr.Validate(tt.variableName) + suite.Equal(tt.expected, result) + }) + } +} + +func (suite *DisplayCtrlrTestSuite) TestThreadSafety() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameDisplayMessageSupportedFormats, + variables.VariableTypeString, + "ASCII", + ) + suite.ctrlr.variables[VariableNameDisplayMessageSupportedFormats] = *testVar + + // Test concurrent access to the controller + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func() { + // Concurrent reads + _, _ = suite.ctrlr.GetVariable(VariableNameDisplayMessageSupportedFormats) + _ = suite.ctrlr.Validate(VariableNameDisplayMessageSupportedFormats) + _ = suite.ctrlr.GetRequiredVariables() + _ = suite.ctrlr.GetSupportedVariables() + + // Concurrent writes + _ = suite.ctrlr.UpdateVariable(VariableNameDisplayMessageSupportedFormats, "value", "UTF8") + + done <- true + }() + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Verify the controller is still in a valid state + suite.True(suite.ctrlr.Validate(VariableNameDisplayMessageSupportedFormats)) +} + +func TestDisplayCtrlrTestSuite(t *testing.T) { + suite.Run(t, new(DisplayCtrlrTestSuite)) +} diff --git a/ocpp_v201/controllers/iso15118_ctrlr.go b/ocpp_v201/controllers/iso15118_ctrlr.go new file mode 100644 index 0000000..4316cec --- /dev/null +++ b/ocpp_v201/controllers/iso15118_ctrlr.go @@ -0,0 +1,153 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameCentralContractValidationAllowed variables.VariableName = "CentralContractValidationAllowed" + VariableNameContractValidationOffline variables.VariableName = "ContractValidationOffline" + VariableNameProtocolSupportedByEV variables.VariableName = "ProtocolSupportedByEV" + VariableNameProtocolAgreed variables.VariableName = "ProtocolAgreed" + VariableNameISO15118PnCEnabled variables.VariableName = "PnCEnabled" + VariableNameISO15118V2GCertificateInstallationEnabled variables.VariableName = "V2GCertificateInstallationEnabled" + VariableNameISO15118ContractCertificateInstallationEnabled variables.VariableName = "ContractCertificateInstallationEnabled" + VariableNameISO15118RequestMeteringReceipt variables.VariableName = "RequestMeteringReceipt" + VariableNameISO15118SeccId variables.VariableName = "SeccId" + VariableNameISO15118CountryName variables.VariableName = "CountryName" + VariableNameISO15118EvseId variables.VariableName = "ISO15118EvseId" +) + +func requiredVariablesISO15118Ctrlr() []variables.VariableName { + return []variables.VariableName{ + VariableNameCentralContractValidationAllowed, + VariableNameContractValidationOffline, + VariableNameProtocolSupportedByEV, + VariableNameProtocolAgreed, + } +} + +func optionalVariablesISO15118Ctrlr() []variables.VariableName { + return []variables.VariableName{ + VariableNameISO15118PnCEnabled, + VariableNameISO15118V2GCertificateInstallationEnabled, + VariableNameISO15118ContractCertificateInstallationEnabled, + VariableNameISO15118RequestMeteringReceipt, + VariableNameISO15118SeccId, + VariableNameISO15118CountryName, + VariableNameISO15118EvseId, + } +} + +func supportedVariablesISO15118Ctrlr() []variables.VariableName { + return append(requiredVariablesISO15118Ctrlr(), optionalVariablesISO15118Ctrlr()...) +} + +type ISO15118Ctrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (I *ISO15118Ctrlr) GetName() component.ComponentName { + return component.ComponentNameISO15118Ctrlr +} + +func (I *ISO15118Ctrlr) GetInstanceId() string { + return I.instanceId +} + +func (I *ISO15118Ctrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (I *ISO15118Ctrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (I *ISO15118Ctrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (I *ISO15118Ctrlr) GetRequiredVariables() []variables.VariableName { + I.mu.RLock() + defer I.mu.RUnlock() + + return I.requiredVariables +} + +func (I *ISO15118Ctrlr) GetSupportedVariables() []variables.VariableName { + I.mu.RLock() + defer I.mu.RUnlock() + + return I.supportedVariables +} + +func (I *ISO15118Ctrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !I.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + I.mu.RLock() + defer I.mu.RUnlock() + + variable, exists := I.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (I *ISO15118Ctrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !I.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + I.mu.Lock() + defer I.mu.Unlock() + + v, exists := I.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (I *ISO15118Ctrlr) Validate(key variables.VariableName) bool { + if !I.validator.IsVariableSupported(key) { + return false + } + + I.mu.RLock() + defer I.mu.RUnlock() + + v, exists := I.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewISO15118Ctrlr() *ISO15118Ctrlr { + ctrlr := &ISO15118Ctrlr{ + mu: sync.RWMutex{}, + variables: map[variables.VariableName]*variables.Variable{}, + requiredVariables: requiredVariablesISO15118Ctrlr(), + supportedVariables: supportedVariablesISO15118Ctrlr(), + instanceId: "iso15118-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/iso15118_ctrlr_test.go b/ocpp_v201/controllers/iso15118_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/iso15118_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/local_auth_list_ctrlr.go b/ocpp_v201/controllers/local_auth_list_ctrlr.go new file mode 100644 index 0000000..b574e5d --- /dev/null +++ b/ocpp_v201/controllers/local_auth_list_ctrlr.go @@ -0,0 +1,144 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameLocalAuthListEnabled variables.VariableName = "Enabled" + VariableNameLocalAuthListEntries variables.VariableName = "Entries" + VariableNameLocalAuthListItemsPerMessage variables.VariableName = "ItemsPerMessage" + VariableNameLocalAuthListBytesPerMessage variables.VariableName = "BytesPerMessage" + VariableNameLocalAuthListStorage variables.VariableName = "Storage" + VariableNameLocalAuthListDisablePostAuthorize variables.VariableName = "DisablePostAuthorize" + VariableNameLocalAuthListSupportsExpiryDateTime variables.VariableName = "SupportsExpiryDateTime" +) + +func requiredLocalAuthListVariables() []variables.VariableName { + return []variables.VariableName{} +} + +func optionalLocalAuthListVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameLocalAuthListItemsPerMessage, + VariableNameLocalAuthListBytesPerMessage, + VariableNameLocalAuthListStorage, + VariableNameLocalAuthListDisablePostAuthorize, + VariableNameLocalAuthListSupportsExpiryDateTime, + VariableNameLocalAuthListEnabled, + VariableNameLocalAuthListEntries, + } +} + +func supportedLocalAuthListVariables() []variables.VariableName { + return append(requiredLocalAuthListVariables(), optionalLocalAuthListVariables()...) +} + +type LocalAuthListCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (l *LocalAuthListCtrlr) GetName() component.ComponentName { + return component.ComponentNameLocalAuthListCtrlr +} + +func (l *LocalAuthListCtrlr) GetInstanceId() string { + return l.instanceId +} + +func (l *LocalAuthListCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (l *LocalAuthListCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (l *LocalAuthListCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (l *LocalAuthListCtrlr) GetRequiredVariables() []variables.VariableName { + l.mu.RLock() + defer l.mu.RUnlock() + + return l.requiredVariables +} + +func (l *LocalAuthListCtrlr) GetSupportedVariables() []variables.VariableName { + l.mu.RLock() + defer l.mu.RUnlock() + + return l.supportedVariables +} + +func (l *LocalAuthListCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !l.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + l.mu.RLock() + defer l.mu.RUnlock() + + variable, exists := l.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (l *LocalAuthListCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !l.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + l.mu.Lock() + defer l.mu.Unlock() + + v, exists := l.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (l *LocalAuthListCtrlr) Validate(key variables.VariableName) bool { + if !l.validator.IsVariableSupported(key) { + return false + } + + l.mu.RLock() + defer l.mu.RUnlock() + + v, exists := l.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewLocalAuthListCtrlr() *LocalAuthListCtrlr { + ctrlr := &LocalAuthListCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredLocalAuthListVariables(), + supportedVariables: supportedLocalAuthListVariables(), + instanceId: "local-auth-list-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/local_auth_list_ctrlr_test.go b/ocpp_v201/controllers/local_auth_list_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/local_auth_list_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/monitoring_ctrlr.go b/ocpp_v201/controllers/monitoring_ctrlr.go new file mode 100644 index 0000000..65ee564 --- /dev/null +++ b/ocpp_v201/controllers/monitoring_ctrlr.go @@ -0,0 +1,149 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameMonitoringEnabled variables.VariableName = "Enabled" + VariableNameMonitoringAvailable variables.VariableName = "Available" + VariableNameItemsPerMessageClearVariableMonitoring variables.VariableName = "ItemsPerMessage" + VariableNameItemsPerMessageSetVariableMonitoring variables.VariableName = "ItemsPerMessage" + VariableNameClearVariableMonitoring variables.VariableName = "BytesPerMessage" + VariableNameBytesPerMessageSetVariableMonitoring variables.VariableName = "BytesPerMessage" + VariableNameOfflineMonitoringEventQueuingSeverity variables.VariableName = "OfflineQueuingSeverity" + VariableNameActiveMonitoringBase variables.VariableName = "ActiveMonitoringBase" + VariableNameActiveMonitoringLevel variables.VariableName = "ActiveMonitoringLevel" +) + +func requiredMonitoringVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameItemsPerMessageSetVariableMonitoring, + VariableNameBytesPerMessageSetVariableMonitoring, + } +} + +func optionalMonitoringVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameClearVariableMonitoring, + VariableNameMonitoringEnabled, + VariableNameMonitoringAvailable, + VariableNameItemsPerMessageClearVariableMonitoring, + VariableNameOfflineMonitoringEventQueuingSeverity, + VariableNameActiveMonitoringBase, + VariableNameActiveMonitoringLevel, + } +} + +func supportedMonitoringVariables() []variables.VariableName { + return append(requiredMonitoringVariables(), optionalMonitoringVariables()...) +} + +type MonitoringCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (m *MonitoringCtrlr) GetName() component.ComponentName { + return component.ComponentNameMonitoringCtrlr +} + +func (m *MonitoringCtrlr) GetInstanceId() string { + return m.instanceId +} + +func (m *MonitoringCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (m *MonitoringCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (m *MonitoringCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (m *MonitoringCtrlr) GetRequiredVariables() []variables.VariableName { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.requiredVariables +} + +func (m *MonitoringCtrlr) GetSupportedVariables() []variables.VariableName { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.supportedVariables +} + +func (m *MonitoringCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !m.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + m.mu.RLock() + defer m.mu.RUnlock() + + variable, exists := m.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (m *MonitoringCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !m.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + m.mu.Lock() + defer m.mu.Unlock() + + v, exists := m.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (m *MonitoringCtrlr) Validate(key variables.VariableName) bool { + if !m.validator.IsVariableSupported(key) { + return false + } + + m.mu.RLock() + defer m.mu.RUnlock() + + v, exists := m.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewMonitoringCtrlr() *MonitoringCtrlr { + ctrlr := &MonitoringCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredMonitoringVariables(), + supportedVariables: supportedMonitoringVariables(), + instanceId: "monitoring-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + return ctrlr +} diff --git a/ocpp_v201/controllers/monitoring_ctrlr_test.go b/ocpp_v201/controllers/monitoring_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/monitoring_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/ocpp_comm_ctrlr.go b/ocpp_v201/controllers/ocpp_comm_ctrlr.go new file mode 100644 index 0000000..86fccf5 --- /dev/null +++ b/ocpp_v201/controllers/ocpp_comm_ctrlr.go @@ -0,0 +1,167 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameDefaultMessageTimeout variables.VariableName = "DefaultMessageTimeout" + VariableNameNetworkProfileConnectionAttempts variables.VariableName = "NetworkProfileConnectionAttempts" + VariableNameNetworkConfigurationPriority variables.VariableName = "NetworkConfigurationPriority" + VariableNameHeartbeatInterval variables.VariableName = "HeartbeatInterval" + VariableNameFileTransferProtocols variables.VariableName = "FileTransferProtocols" + VariableNameMessageTimeout variables.VariableName = "MessageTimeout" + VariableNameActiveNetworkProfile variables.VariableName = "ActiveNetworkProfile" + VariableNameOfflineThreshold variables.VariableName = "OfflineThreshold" + VariableNameQueueAllMessages variables.VariableName = "QueueAllMessages" + VariableNameMessageAttempts variables.VariableName = "MessageAttempts" + VariableNameMessageAttemptInterval variables.VariableName = "MessageAttemptInterval" + VariableNameMessageAttemptsTransactionEvent variables.VariableName = "MessageAttemptsTransactionEvent" + VariableNameMessageAttemptIntervalTransactionEvent variables.VariableName = "MessageAttemptIntervalTransactionEvent" + VariableNameUnlockOnEVSideDisconnect variables.VariableName = "UnlockOnEVSideDisconnect" + VariableNameWebSocketPingInterval variables.VariableName = "WebSocketPingInterval" + VariableNameResetRetries variables.VariableName = "ResetRetries" + VariableNameFieldLength variables.VariableName = "FieldLength" + VariableNamePublicKeyWithSignedMeterValue variables.VariableName = "PublicKeyWithSignedMeterValue" +) + +func requiredOcppCommCtrlrVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameDefaultMessageTimeout, + VariableNameFileTransferProtocols, + VariableNameOfflineThreshold, + VariableNameNetworkProfileConnectionAttempts, + VariableNameMessageAttempts, + VariableNameMessageAttemptInterval, + VariableNameMessageAttemptsTransactionEvent, + VariableNameMessageAttemptIntervalTransactionEvent, + VariableNameUnlockOnEVSideDisconnect, + VariableNameResetRetries, + } +} + +func optionalOcppCommCtrlrVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameNetworkConfigurationPriority, + VariableNameHeartbeatInterval, + VariableNameMessageTimeout, + VariableNameActiveNetworkProfile, + VariableNameQueueAllMessages, + VariableNameWebSocketPingInterval, + VariableNameFieldLength, + VariableNamePublicKeyWithSignedMeterValue, + } +} + +func supportedOcppCommCtrlrVariables() []variables.VariableName { + return append(requiredOcppCommCtrlrVariables(), optionalOcppCommCtrlrVariables()...) +} + +type OCPPCommCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (ctrlr *OCPPCommCtrlr) GetName() component.ComponentName { + return component.ComponentNameOCPPCommCtrlr +} + +func (ctrlr *OCPPCommCtrlr) GetInstanceId() string { + return ctrlr.instanceId +} + +func (ctrlr *OCPPCommCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (ctrlr *OCPPCommCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (ctrlr *OCPPCommCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (ctrlr *OCPPCommCtrlr) GetRequiredVariables() []variables.VariableName { + ctrlr.mu.RLock() + defer ctrlr.mu.RUnlock() + + return ctrlr.requiredVariables +} + +func (ctrlr *OCPPCommCtrlr) GetSupportedVariables() []variables.VariableName { + ctrlr.mu.RLock() + defer ctrlr.mu.RUnlock() + + return ctrlr.supportedVariables +} + +func (ctrlr *OCPPCommCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !ctrlr.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + ctrlr.mu.RLock() + defer ctrlr.mu.RUnlock() + + variable, exists := ctrlr.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (ctrlr *OCPPCommCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !ctrlr.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + ctrlr.mu.Lock() + defer ctrlr.mu.Unlock() + + v, exists := ctrlr.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (ctrlr *OCPPCommCtrlr) Validate(key variables.VariableName) bool { + if !ctrlr.validator.IsVariableSupported(key) { + return false + } + + ctrlr.mu.RLock() + defer ctrlr.mu.RUnlock() + + v, exists := ctrlr.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewOCPPCommCtrlr() *OCPPCommCtrlr { + ctrlr := &OCPPCommCtrlr{ + mu: sync.RWMutex{}, + variables: map[variables.VariableName]*variables.Variable{}, + requiredVariables: requiredOcppCommCtrlrVariables(), + supportedVariables: supportedOcppCommCtrlrVariables(), + instanceId: "ocpp-comm-ctrlr-1", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/ocpp_comm_ctrlr_test.go b/ocpp_v201/controllers/ocpp_comm_ctrlr_test.go new file mode 100644 index 0000000..0d171e8 --- /dev/null +++ b/ocpp_v201/controllers/ocpp_comm_ctrlr_test.go @@ -0,0 +1,299 @@ +package controllers + +import ( + "testing" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + "github.com/stretchr/testify/suite" +) + +type OCPPCommCtrlrTestSuite struct { + suite.Suite + ctrlr *OCPPCommCtrlr +} + +func (suite *OCPPCommCtrlrTestSuite) SetupTest() { + suite.ctrlr = NewOCPPCommCtrlr() +} + +func (suite *OCPPCommCtrlrTestSuite) TestNewOCPPCommCtrlr() { + ctrlr := NewOCPPCommCtrlr() + + suite.NotNil(ctrlr) + suite.Equal(component.ComponentNameOCPPCommCtrlr, ctrlr.GetName()) + suite.Equal("ocpp-comm-ctrlr-1", ctrlr.GetInstanceId()) + suite.NotNil(ctrlr.validator) + suite.NotEmpty(ctrlr.GetRequiredVariables()) + suite.NotEmpty(ctrlr.GetSupportedVariables()) +} + +func (suite *OCPPCommCtrlrTestSuite) TestGetName() { + suite.Equal(component.ComponentNameOCPPCommCtrlr, suite.ctrlr.GetName()) +} + +func (suite *OCPPCommCtrlrTestSuite) TestGetInstanceId() { + suite.Equal("ocpp-comm-ctrlr-1", suite.ctrlr.GetInstanceId()) +} + +func (suite *OCPPCommCtrlrTestSuite) TestSubComponentMethods() { + // Test that sub-component methods are no-op + initialCount := len(suite.ctrlr.GetSubComponents()) + + // Register should be no-op + suite.ctrlr.RegisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "RegisterSubComponent should be a no-op") + + // Unregister should be no-op + suite.ctrlr.UnregisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "UnregisterSubComponent should be a no-op") +} + +func (suite *OCPPCommCtrlrTestSuite) TestGetSubComponents() { + subComponents := suite.ctrlr.GetSubComponents() + suite.Equal(0, len(subComponents)) +} + +func (suite *OCPPCommCtrlrTestSuite) TestGetRequiredVariables() { + requiredVars := suite.ctrlr.GetRequiredVariables() + suite.NotEmpty(requiredVars) + + expectedVars := []variables.VariableName{ + VariableNameDefaultMessageTimeout, + VariableNameFileTransferProtocols, + VariableNameOfflineThreshold, + VariableNameNetworkProfileConnectionAttempts, + VariableNameMessageAttempts, + VariableNameMessageAttemptInterval, + VariableNameMessageAttemptsTransactionEvent, + VariableNameMessageAttemptIntervalTransactionEvent, + VariableNameUnlockOnEVSideDisconnect, + VariableNameResetRetries, + } + + for _, expectedVar := range expectedVars { + suite.Contains(requiredVars, expectedVar) + } +} + +func (suite *OCPPCommCtrlrTestSuite) TestGetSupportedVariables() { + supportedVars := suite.ctrlr.GetSupportedVariables() + suite.NotEmpty(supportedVars) + + // Should include all required variables + requiredVars := suite.ctrlr.GetRequiredVariables() + for _, requiredVar := range requiredVars { + suite.Contains(supportedVars, requiredVar) + } + + // Should include optional variables + optionalVars := []variables.VariableName{ + VariableNameNetworkConfigurationPriority, + VariableNameHeartbeatInterval, + VariableNameMessageTimeout, + VariableNameActiveNetworkProfile, + VariableNameQueueAllMessages, + VariableNameWebSocketPingInterval, + VariableNameFieldLength, + VariableNamePublicKeyWithSignedMeterValue, + } + + for _, optionalVar := range optionalVars { + suite.Contains(supportedVars, optionalVar) + } +} + +func (suite *OCPPCommCtrlrTestSuite) TestGetVariable() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameHeartbeatInterval, + variables.VariableTypeInteger, + int64(300), + ) + suite.ctrlr.variables[VariableNameHeartbeatInterval] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + expectError bool + errorContains string + }{ + { + name: "existing variable", + variableName: VariableNameHeartbeatInterval, + expectError: false, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + expectError: true, + errorContains: "not found", + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result, err := suite.ctrlr.GetVariable(tt.variableName) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + suite.Nil(result) + } else { + suite.NoError(err) + suite.NotNil(result) + suite.Equal(tt.variableName, result.Name) + } + }) + } +} + +func (suite *OCPPCommCtrlrTestSuite) TestUpdateVariable() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameHeartbeatInterval, + variables.VariableTypeInteger, + int64(300), + ) + suite.ctrlr.variables[VariableNameHeartbeatInterval] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + attribute string + value interface{} + expectError bool + errorContains string + }{ + { + name: "update existing variable", + variableName: VariableNameHeartbeatInterval, + attribute: "value", + value: int64(600), + expectError: false, + }, + { + name: "update non-existent variable", + variableName: "NonExistentVariable", + attribute: "value", + value: int64(600), + expectError: true, + errorContains: "not found", + }, + { + name: "update unsupported variable", + variableName: "UnsupportedVariable", + attribute: "value", + value: int64(600), + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + err := suite.ctrlr.UpdateVariable(tt.variableName, tt.attribute, tt.value) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + } else { + suite.NoError(err) + + // Verify the update worked + variable, err := suite.ctrlr.GetVariable(tt.variableName) + suite.NoError(err) + suite.NotNil(variable) + } + }) + } +} + +func (suite *OCPPCommCtrlrTestSuite) TestValidate() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameHeartbeatInterval, + variables.VariableTypeInteger, + int64(300), + ) + suite.ctrlr.variables[VariableNameHeartbeatInterval] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + expected bool + }{ + { + name: "valid existing variable", + variableName: VariableNameHeartbeatInterval, + expected: true, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + expected: false, + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + expected: false, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result := suite.ctrlr.Validate(tt.variableName) + suite.Equal(tt.expected, result) + }) + } +} + +func (suite *OCPPCommCtrlrTestSuite) TestThreadSafety() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameHeartbeatInterval, + variables.VariableTypeInteger, + int64(300), + ) + suite.ctrlr.variables[VariableNameHeartbeatInterval] = *testVar + + // Test concurrent access to the controller + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func() { + // Concurrent reads + _, _ = suite.ctrlr.GetVariable(VariableNameHeartbeatInterval) + _ = suite.ctrlr.Validate(VariableNameHeartbeatInterval) + _ = suite.ctrlr.GetRequiredVariables() + _ = suite.ctrlr.GetSupportedVariables() + + // Concurrent writes + _ = suite.ctrlr.UpdateVariable(VariableNameHeartbeatInterval, "value", int64(600)) + + done <- true + }() + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Verify the controller is still in a valid state + suite.True(suite.ctrlr.Validate(VariableNameHeartbeatInterval)) +} + +func TestOCPPCommCtrlrTestSuite(t *testing.T) { + suite.Run(t, new(OCPPCommCtrlrTestSuite)) +} diff --git a/ocpp_v201/controllers/reservation_ctrlr.go b/ocpp_v201/controllers/reservation_ctrlr.go new file mode 100644 index 0000000..5fdb908 --- /dev/null +++ b/ocpp_v201/controllers/reservation_ctrlr.go @@ -0,0 +1,131 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameReservationEnabled variables.VariableName = "Enabled" + VariableNameReservationAvailable variables.VariableName = "Available" + VariableNameReservationNonEvseSpecific variables.VariableName = "NonEvseSpecific" +) + +func optionalReservationVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameReservationEnabled, + VariableNameReservationAvailable, + VariableNameReservationNonEvseSpecific, + } +} + +func supportedReservationVariables() []variables.VariableName { + return optionalReservationVariables() +} + +type ReservationCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (r *ReservationCtrlr) GetName() component.ComponentName { + return component.ComponentNameReservationCtrlr +} + +func (r *ReservationCtrlr) GetInstanceId() string { + return r.instanceId +} + +func (r *ReservationCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (r *ReservationCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (r *ReservationCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (r *ReservationCtrlr) GetRequiredVariables() []variables.VariableName { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.requiredVariables +} + +func (r *ReservationCtrlr) GetSupportedVariables() []variables.VariableName { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.supportedVariables +} + +func (r *ReservationCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !r.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + r.mu.RLock() + defer r.mu.RUnlock() + + variable, exists := r.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (r *ReservationCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !r.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + r.mu.Lock() + defer r.mu.Unlock() + + v, exists := r.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (r *ReservationCtrlr) Validate(key variables.VariableName) bool { + if !r.validator.IsVariableSupported(key) { + return false + } + + r.mu.RLock() + defer r.mu.RUnlock() + + v, exists := r.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewReservationCtrlr() *ReservationCtrlr { + ctrlr := &ReservationCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: []variables.VariableName{}, // No required variables for ReservationCtrlr + supportedVariables: supportedReservationVariables(), + instanceId: "reservation-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + return ctrlr +} diff --git a/ocpp_v201/controllers/reservation_ctrlr_test.go b/ocpp_v201/controllers/reservation_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/reservation_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/sampled_data_ctrlr.go b/ocpp_v201/controllers/sampled_data_ctrlr.go new file mode 100644 index 0000000..54cb070 --- /dev/null +++ b/ocpp_v201/controllers/sampled_data_ctrlr.go @@ -0,0 +1,150 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameSampledDataEnabled variables.VariableName = "Enabled" + VariableNameSampledDataAvailable variables.VariableName = "Available" + VariableNameSampledDataSignReadings variables.VariableName = "SignReadings" + VariableNameSampledDataTxEndedMeasurands variables.VariableName = "TxEndedMeasurands" + VariableNameSampledDataTxEndedInterval variables.VariableName = "TxEndedInterval" + VariableNameSampledDataTxStartedMeasurands variables.VariableName = "TxStartedMeasurands" + VariableNameSampledDataTxUpdatedMeasurands variables.VariableName = "TxUpdatedMeasurands" + VariableNameSampledDataTxUpdatedInterval variables.VariableName = "TxUpdatedInterval" + VariableNameSampledDataRegisterValuesWithoutPhases variables.VariableName = "RegisterValuesWithoutPhases" +) + +func requiredSampledDataVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameSampledDataTxEndedMeasurands, + VariableNameSampledDataTxEndedInterval, + VariableNameSampledDataTxStartedMeasurands, + VariableNameSampledDataTxUpdatedMeasurands, + VariableNameSampledDataTxUpdatedInterval, + } +} + +func optionalSampledDataVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameSampledDataEnabled, + VariableNameSampledDataAvailable, + VariableNameSampledDataSignReadings, + VariableNameSampledDataRegisterValuesWithoutPhases, + } +} + +func supportedSampledDataVariables() []variables.VariableName { + return append(requiredSampledDataVariables(), optionalSampledDataVariables()...) +} + +type SampledDataCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (s *SampledDataCtrlr) GetName() component.ComponentName { + return component.ComponentNameSampledDataCtrlr +} + +func (s *SampledDataCtrlr) GetInstanceId() string { + return s.instanceId +} + +func (s *SampledDataCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (s *SampledDataCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (s *SampledDataCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (s *SampledDataCtrlr) GetRequiredVariables() []variables.VariableName { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.requiredVariables +} + +func (s *SampledDataCtrlr) GetSupportedVariables() []variables.VariableName { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.supportedVariables +} + +func (s *SampledDataCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !s.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + variable, exists := s.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (s *SampledDataCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !s.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + s.mu.Lock() + defer s.mu.Unlock() + + v, exists := s.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (s *SampledDataCtrlr) Validate(key variables.VariableName) bool { + if !s.validator.IsVariableSupported(key) { + return false + } + + s.mu.Lock() + defer s.mu.Unlock() + + v, exists := s.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewSampledDataCtrlr() *SampledDataCtrlr { + ctrlr := &SampledDataCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredSampledDataVariables(), + supportedVariables: supportedSampledDataVariables(), + instanceId: "sampled-data-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/sampled_data_ctrlr_test.go b/ocpp_v201/controllers/sampled_data_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/sampled_data_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/security_ctrlr.go b/ocpp_v201/controllers/security_ctrlr.go new file mode 100644 index 0000000..97e9486 --- /dev/null +++ b/ocpp_v201/controllers/security_ctrlr.go @@ -0,0 +1,150 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameBasicAuthPassword variables.VariableName = "BasicAuthPassword" + VariableNameIdentity variables.VariableName = "Identity" + VariableNameOrganizationName variables.VariableName = "OrganizationName" + VariableNameCertificateEntries variables.VariableName = "CertificateEntries" + VariableNameAdditionalRootCertificateCheck variables.VariableName = "AdditionalRootCertificateCheck" + VariableNameSecurityProfile variables.VariableName = "SecurityProfile" + VariableNameMaxCertificateChainSize variables.VariableName = "MaxCertificateChainSize" + VariableNameCertSigningWaitMinimum variables.VariableName = "CertSigningWaitMinimum" + VariableNameCertSigningRepeatTimes variables.VariableName = "CertSigningRepeatTimes" +) + +func requiredSecurityVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameSecurityProfile, + } +} + +func optionalSecurityVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameBasicAuthPassword, + VariableNameIdentity, + VariableNameAdditionalRootCertificateCheck, + VariableNameCertSigningRepeatTimes, + VariableNameMaxCertificateChainSize, + VariableNameCertSigningWaitMinimum, + VariableNameCertificateEntries, + VariableNameOrganizationName, + } +} + +func supportedSecurityVariables() []variables.VariableName { + return append(requiredSecurityVariables(), optionalSecurityVariables()...) +} + +type SecurityCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (s *SecurityCtrlr) GetName() component.ComponentName { + return component.ComponentNameSecurityCtrlr +} + +func (s *SecurityCtrlr) GetInstanceId() string { + return s.instanceId +} + +func (s *SecurityCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (s *SecurityCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (s *SecurityCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (s *SecurityCtrlr) GetRequiredVariables() []variables.VariableName { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.requiredVariables +} + +func (s *SecurityCtrlr) GetSupportedVariables() []variables.VariableName { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.supportedVariables +} + +func (s *SecurityCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !s.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + variable, exists := s.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (s *SecurityCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !s.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + v, exists := s.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (s *SecurityCtrlr) Validate(key variables.VariableName) bool { + if !s.validator.IsVariableSupported(key) { + return false + } + + s.mu.RLock() + defer s.mu.RUnlock() + + v, exists := s.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewSecurityCtrlr() *SecurityCtrlr { + ctrlr := &SecurityCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredSecurityVariables(), + supportedVariables: supportedSecurityVariables(), + instanceId: "security-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/security_ctrlr_test.go b/ocpp_v201/controllers/security_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/security_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/smart_charging_ctrlr.go b/ocpp_v201/controllers/smart_charging_ctrlr.go new file mode 100644 index 0000000..cd4b05a --- /dev/null +++ b/ocpp_v201/controllers/smart_charging_ctrlr.go @@ -0,0 +1,154 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameSmartChargingEnabled variables.VariableName = "Enabled" + VariableNameSmartChargingAvailable variables.VariableName = "Available" + VariableNameACPhaseSwitchingSupported variables.VariableName = "ACPhaseSwitchingSupported" + VariableNameChargingProfileStackLevel variables.VariableName = "ProfileStackLevel" + VariableNameChargingScheduleChargingRateUnit variables.VariableName = "RateUnit" + VariableNamePeriodsPerSchedule variables.VariableName = "PeriodsPerSchedule" + VariableNameExternalControlSignalsEnabled variables.VariableName = "ExternalControlSignalsEnabled" + VariableNameNotifyChargingLimitWithSchedules variables.VariableName = "NotifyChargingLimitWithSchedules" + VariableNamePhases3to1 variables.VariableName = "Phases3to1" + VariableChargingProfileEntries variables.VariableName = "Entries" + VariableLimitChangeSignificance variables.VariableName = "LimitChangeSignificance" +) + +func requiredSmartChargingVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameChargingProfileStackLevel, + VariableNameChargingScheduleChargingRateUnit, + VariableNamePeriodsPerSchedule, + VariableChargingProfileEntries, + VariableLimitChangeSignificance, + } +} + +func optionalSmartChargingVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameSmartChargingEnabled, + VariableNameSmartChargingAvailable, + VariableNameACPhaseSwitchingSupported, + VariableNameExternalControlSignalsEnabled, + VariableNameNotifyChargingLimitWithSchedules, + VariableNamePhases3to1, + } +} + +func supportedSmartChargingVariables() []variables.VariableName { + return append(requiredSmartChargingVariables(), optionalSmartChargingVariables()...) +} + +type SmartChargingCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (s *SmartChargingCtrlr) GetName() component.ComponentName { + return component.ComponentNameSmartChargingCtrlr +} + +func (s *SmartChargingCtrlr) GetInstanceId() string { + return s.instanceId +} + +func (s *SmartChargingCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (s *SmartChargingCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (s *SmartChargingCtrlr) GetSubComponents() []component.Component { + // Controllers do not support sub-components, always return empty slice + return []component.Component{} +} + +func (s *SmartChargingCtrlr) GetRequiredVariables() []variables.VariableName { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.requiredVariables +} + +func (s *SmartChargingCtrlr) GetSupportedVariables() []variables.VariableName { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.supportedVariables +} + +func (s *SmartChargingCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !s.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + s.mu.RLock() + defer s.mu.RUnlock() + + variable, exists := s.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + + return variable, nil +} + +func (s *SmartChargingCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !s.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + s.mu.Lock() + defer s.mu.Unlock() + + v, exists := s.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (s *SmartChargingCtrlr) Validate(key variables.VariableName) bool { + if !s.validator.IsVariableSupported(key) { + return false + } + + s.mu.RLock() + defer s.mu.RUnlock() + + v, exists := s.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewSmartChargingCtrlr() *SmartChargingCtrlr { + ctrlr := &SmartChargingCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredSmartChargingVariables(), + supportedVariables: supportedSmartChargingVariables(), + instanceId: "smart-charging-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/smart_charging_ctrlr_test.go b/ocpp_v201/controllers/smart_charging_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/smart_charging_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/tariff_cost_ctrlr.go b/ocpp_v201/controllers/tariff_cost_ctrlr.go new file mode 100644 index 0000000..8bfbb68 --- /dev/null +++ b/ocpp_v201/controllers/tariff_cost_ctrlr.go @@ -0,0 +1,148 @@ +package controllers + +import ( + "errors" + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameTariffEnabled variables.VariableName = "Enabled" + VariableNameTariffAvailable variables.VariableName = "Available" + VariableNameTariffFallbackMessage variables.VariableName = "TariffFallbackMessage" + VariableNameCostEnabled variables.VariableName = "Enabled" + VariableNameCostAvailable variables.VariableName = "Available" + VariableNameTotalCostFallbackMessage variables.VariableName = "TotalCostFallbackMessage" + VariableNameCurrency variables.VariableName = "Currency" +) + +func requiredVariablesTariffCostCtrlr() []variables.VariableName { + return []variables.VariableName{ + VariableNameTariffFallbackMessage, + VariableNameTotalCostFallbackMessage, + VariableNameCurrency, + } +} + +func optionalVariablesTariffCostCtrlr() []variables.VariableName { + return []variables.VariableName{ + VariableNameTariffEnabled, + VariableNameTariffAvailable, + VariableNameCostEnabled, + VariableNameCostAvailable, + } +} + +func supportedVariablesTariffCostCtrlr() []variables.VariableName { + return append(requiredVariablesTariffCostCtrlr(), optionalVariablesTariffCostCtrlr()...) +} + +type TariffCostCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + instanceId string + validator *variableValidator +} + +func (t *TariffCostCtrlr) GetName() component.ComponentName { + return component.ComponentNameTariffCostCtrlr +} + +func (t *TariffCostCtrlr) GetInstanceId() string { + return t.instanceId +} + +func (t *TariffCostCtrlr) RegisterSubComponent(component component.Component) { +} + +func (t *TariffCostCtrlr) UnregisterSubComponent(component component.Component) { +} + +func (t *TariffCostCtrlr) GetSubComponents() []component.Component { + return []component.Component{} +} + +func (t *TariffCostCtrlr) GetRequiredVariables() []variables.VariableName { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.requiredVariables +} + +func (t *TariffCostCtrlr) GetSupportedVariables() []variables.VariableName { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.supportedVariables +} + +func (t *TariffCostCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !t.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported", key) + } + + t.mu.RLock() + defer t.mu.RUnlock() + + variable, exists := t.variables[key] + if !exists { + return nil, errors.New("variable does not exist") + } + + return variable, nil +} + +func (t *TariffCostCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !t.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported", variable) + } + + t.mu.Lock() + defer t.mu.Unlock() + if _, exists := t.variables[variable]; !exists { + return errors.New("variable does not exist") + } + + variableInstance := t.variables[variable] + err := variableInstance.UpdateVariableAttribute(attribute, value) + if err != nil { + return err + } + + t.variables[variable] = variableInstance + return nil +} + +func (t *TariffCostCtrlr) Validate(key variables.VariableName) bool { + if !t.validator.IsVariableSupported(key) { + return false + } + + t.mu.RLock() + defer t.mu.RUnlock() + variable, exists := t.variables[key] + if !exists { + return false + } + + return variable.Validate() +} + +func NewTariffCostCtrlr() *TariffCostCtrlr { + ctrlr := &TariffCostCtrlr{ + mu: sync.RWMutex{}, + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredVariablesTariffCostCtrlr(), + supportedVariables: supportedVariablesTariffCostCtrlr(), + instanceId: "tariff-cost-ctrlr", + } + + ctrlr.validator = newVariableValidator(ctrlr) + + return ctrlr +} diff --git a/ocpp_v201/controllers/tariff_cost_ctrlr_test.go b/ocpp_v201/controllers/tariff_cost_ctrlr_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/tariff_cost_ctrlr_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/controllers/tx_ctrlr.go b/ocpp_v201/controllers/tx_ctrlr.go new file mode 100644 index 0000000..0d3a2a8 --- /dev/null +++ b/ocpp_v201/controllers/tx_ctrlr.go @@ -0,0 +1,138 @@ +package controllers + +import ( + "fmt" + "sync" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" +) + +const ( + VariableNameEVConnectionTimeOut variables.VariableName = "EVConnectionTimeOut" + VariableNameStopTxOnEVSideDisconnect variables.VariableName = "StopTxOnEVSideDisconnect" + VariableNameTxBeforeAcceptedEnabled variables.VariableName = "TxBeforeAcceptedEnabled" + VariableNameTxStartPoint variables.VariableName = "TxStartPoint" + VariableNameTxStopPoint variables.VariableName = "TxStopPoint" + VariableNameMaxEnergyOnInvalidId variables.VariableName = "MaxEnergyOnInvalidId" + VariableNameStopTxOnInvalidId variables.VariableName = "StopTxOnInvalidId" +) + +func requiredTxCtrlrVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameTxStartPoint, + VariableNameTxStopPoint, + } +} + +func optionalTxCtrlrVariables() []variables.VariableName { + return []variables.VariableName{ + VariableNameTxBeforeAcceptedEnabled, + VariableNameMaxEnergyOnInvalidId, + } +} + +func supportedTxCtrlrVariables() []variables.VariableName { + return append(requiredTxCtrlrVariables(), optionalTxCtrlrVariables()...) +} + +type TxCtrlr struct { + mu sync.RWMutex + variables map[variables.VariableName]*variables.Variable + requiredVariables []variables.VariableName + supportedVariables []variables.VariableName + validator *variableValidator + instanceId string +} + +func (t *TxCtrlr) GetName() component.ComponentName { + return component.ComponentNameTxCtrlr +} + +func (t *TxCtrlr) GetInstanceId() string { + return t.instanceId +} + +func (t *TxCtrlr) RegisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (t *TxCtrlr) UnregisterSubComponent(component component.Component) { + // No-op: controllers do not support sub-components +} + +func (t *TxCtrlr) GetSubComponents() []component.Component { + return []component.Component{} +} + +func (t *TxCtrlr) GetRequiredVariables() []variables.VariableName { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.requiredVariables +} + +func (t *TxCtrlr) GetSupportedVariables() []variables.VariableName { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.supportedVariables +} + +func (t *TxCtrlr) GetVariable(key variables.VariableName, opts ...component.GetSetVariableOption) (*variables.Variable, error) { + if !t.validator.IsVariableSupported(key) { + return nil, fmt.Errorf("variable %s is not supported by this controller", key) + } + + t.mu.RLock() + defer t.mu.RUnlock() + + variable, exists := t.variables[key] + if !exists { + return nil, fmt.Errorf("variable %s not found", key) + } + return variable, nil +} + +func (t *TxCtrlr) UpdateVariable(variable variables.VariableName, attribute string, value interface{}, opts ...component.GetSetVariableOption) error { + if !t.validator.IsVariableSupported(variable) { + return fmt.Errorf("variable %s is not supported by this controller", variable) + } + + t.mu.Lock() + defer t.mu.Unlock() + + v, exists := t.variables[variable] + if !exists { + return fmt.Errorf("variable %s not found", variable) + } + + return v.UpdateVariableAttribute(attribute, value) +} + +func (t *TxCtrlr) Validate(key variables.VariableName) bool { + + if !t.validator.IsVariableSupported(key) { + return false + } + t.mu.RLock() + defer t.mu.RUnlock() + + v, exists := t.variables[key] + if !exists { + return false + } + + return v.Validate() +} + +func NewTxCtrlr() *TxCtrlr { + ctrlr := &TxCtrlr{ + variables: make(map[variables.VariableName]*variables.Variable), + requiredVariables: requiredTxCtrlrVariables(), + supportedVariables: supportedTxCtrlrVariables(), + instanceId: "tx-ctrlr", + } + ctrlr.validator = newVariableValidator(ctrlr) + return ctrlr +} diff --git a/ocpp_v201/controllers/tx_ctrlr_test.go b/ocpp_v201/controllers/tx_ctrlr_test.go new file mode 100644 index 0000000..4261dca --- /dev/null +++ b/ocpp_v201/controllers/tx_ctrlr_test.go @@ -0,0 +1,269 @@ +package controllers + +import ( + "testing" + + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + "github.com/stretchr/testify/suite" +) + +type TxCtrlrTestSuite struct { + suite.Suite + ctrlr *TxCtrlr +} + +func (suite *TxCtrlrTestSuite) SetupTest() { + suite.ctrlr = NewTxCtrlr() +} + +func (suite *TxCtrlrTestSuite) TestNewTxCtrlr() { + ctrlr := NewTxCtrlr() + + suite.NotNil(ctrlr) + suite.Equal(component.ComponentNameTxCtrlr, ctrlr.GetName()) + suite.Equal("tx-ctrlr", ctrlr.GetInstanceId()) + suite.NotEmpty(ctrlr.GetRequiredVariables()) + suite.NotEmpty(ctrlr.GetSupportedVariables()) +} + +func (suite *TxCtrlrTestSuite) TestGetName() { + suite.Equal(component.ComponentNameTxCtrlr, suite.ctrlr.GetName()) +} + +func (suite *TxCtrlrTestSuite) TestGetInstanceId() { + suite.Equal("tx-ctrlr", suite.ctrlr.GetInstanceId()) +} + +func (suite *TxCtrlrTestSuite) TestSubComponentMethods() { + // Test that sub-component methods are no-op + initialCount := len(suite.ctrlr.GetSubComponents()) + + // Register should be no-op + suite.ctrlr.RegisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "RegisterSubComponent should be a no-op") + + // Unregister should be no-op + suite.ctrlr.UnregisterSubComponent(nil) + suite.Equal(initialCount, len(suite.ctrlr.GetSubComponents()), "UnregisterSubComponent should be a no-op") +} + +func (suite *TxCtrlrTestSuite) TestGetSubComponents() { + subComponents := suite.ctrlr.GetSubComponents() + suite.Equal(0, len(subComponents)) +} + +func (suite *TxCtrlrTestSuite) TestGetRequiredVariables() { + requiredVars := suite.ctrlr.GetRequiredVariables() + expectedVars := requiredTxCtrlrVariables() + suite.NotEmpty(requiredVars) + for _, expectedVar := range expectedVars { + suite.Contains(requiredVars, expectedVar) + } +} + +func (suite *TxCtrlrTestSuite) TestGetSupportedVariables() { + supportedVars := suite.ctrlr.GetSupportedVariables() + suite.NotEmpty(supportedVars) + + // Should include all required variables + requiredVars := suite.ctrlr.GetRequiredVariables() + for _, requiredVar := range requiredVars { + suite.Contains(supportedVars, requiredVar) + } +} + +func (suite *TxCtrlrTestSuite) TestGetVariable() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameTxStartPoint, + variables.VariableTypeString, + "EVConnected", + ) + suite.ctrlr.variables[VariableNameTxStartPoint] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + expectError bool + errorContains string + }{ + { + name: "existing variable", + variableName: VariableNameTxStartPoint, + expectError: false, + }, + { + name: "non-existent variable", + variableName: VariableNameDateTime, + expectError: true, + errorContains: "variable DateTime is not supported by this controller", + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + expectError: true, + errorContains: "variable UnsupportedVariable is not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result, err := suite.ctrlr.GetVariable(tt.variableName) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + suite.Nil(result) + } else { + suite.NoError(err) + suite.NotNil(result) + suite.Equal(tt.variableName, result.Name) + } + }) + } +} + +func (suite *TxCtrlrTestSuite) TestUpdateVariable() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameTxStartPoint, + variables.VariableTypeString, + "EVConnected", + ) + suite.ctrlr.variables[VariableNameTxStartPoint] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + attribute string + value interface{} + expectError bool + errorContains string + }{ + { + name: "update existing variable", + variableName: VariableNameTxStartPoint, + attribute: "value", + value: "Authorized", + expectError: false, + }, + { + name: "update non-existent variable", + variableName: "NonExistentVariable", + attribute: "value", + value: "Authorized", + expectError: true, + errorContains: "variable NonExistentVariable is not supported by this controller", + }, + { + name: "update unsupported variable", + variableName: "UnsupportedVariable", + attribute: "value", + value: "Authorized", + expectError: true, + errorContains: "not supported", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + err := suite.ctrlr.UpdateVariable(tt.variableName, tt.attribute, tt.value) + + if tt.expectError { + suite.Error(err) + if tt.errorContains != "" { + suite.Contains(err.Error(), tt.errorContains) + } + } else { + suite.NoError(err) + + // Verify the update worked + variable, err := suite.ctrlr.GetVariable(tt.variableName) + suite.NoError(err) + suite.NotNil(variable) + } + }) + } +} + +func (suite *TxCtrlrTestSuite) TestValidate() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameTxStartPoint, + variables.VariableTypeString, + "EVConnected", + ) + suite.ctrlr.variables[VariableNameTxStartPoint] = *testVar + + tests := []struct { + name string + variableName variables.VariableName + expected bool + }{ + { + name: "valid existing variable", + variableName: VariableNameTxStartPoint, + expected: true, + }, + { + name: "non-existent variable", + variableName: "NonExistentVariable", + expected: false, + }, + { + name: "unsupported variable", + variableName: "UnsupportedVariable", + expected: false, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + result := suite.ctrlr.Validate(tt.variableName) + suite.Equal(tt.expected, result) + }) + } +} + +func (suite *TxCtrlrTestSuite) TestThreadSafety() { + // Setup a test variable + testVar := variables.NewVariable( + VariableNameTxStartPoint, + variables.VariableTypeString, + "EVConnected", + ) + suite.ctrlr.variables[VariableNameTxStartPoint] = *testVar + + // Test concurrent access to the controller + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func() { + // Concurrent reads + _, _ = suite.ctrlr.GetVariable(VariableNameTxStartPoint) + _ = suite.ctrlr.Validate(VariableNameTxStartPoint) + _ = suite.ctrlr.GetRequiredVariables() + _ = suite.ctrlr.GetSupportedVariables() + + // Concurrent writes + _ = suite.ctrlr.UpdateVariable(VariableNameTxStartPoint, "value", "Authorized") + + done <- true + }() + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Verify the controller is still in a valid state + suite.True(suite.ctrlr.Validate(VariableNameTxStartPoint)) +} + +func TestTxCtrlrTestSuite(t *testing.T) { + suite.Run(t, new(TxCtrlrTestSuite)) +} diff --git a/ocpp_v201/controllers/variable_validator.go b/ocpp_v201/controllers/variable_validator.go new file mode 100644 index 0000000..2cd3e3f --- /dev/null +++ b/ocpp_v201/controllers/variable_validator.go @@ -0,0 +1,23 @@ +package controllers + +import ( + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + "slices" +) + +type variableValidator struct { + component component.Component +} + +func newVariableValidator(component component.Component) *variableValidator { + return &variableValidator{ + component: component, + } +} + +// IsVariableSupported checks if the given variable name is supported by the component. +func (vv *variableValidator) IsVariableSupported(variableName variables.VariableName) bool { + supportedVariables := vv.component.GetSupportedVariables() + return slices.Contains(supportedVariables, variableName) +} diff --git a/ocpp_v201/controllers/variable_validator_test.go b/ocpp_v201/controllers/variable_validator_test.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/ocpp_v201/controllers/variable_validator_test.go @@ -0,0 +1 @@ +package controllers diff --git a/ocpp_v201/ctrlr_manager.go b/ocpp_v201/ctrlr_manager.go new file mode 100644 index 0000000..6c86d62 --- /dev/null +++ b/ocpp_v201/ctrlr_manager.go @@ -0,0 +1,98 @@ +package ocpp_v201 + +import ( + "errors" + "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + "github.com/ChargePi/ocpp-manager/ocpp_v201/controllers" + "github.com/ChargePi/ocpp-manager/ocpp_v201/variables" + "sync" +) + +// Global manager instance +var ( + managerInstance *Manager + managerOnce sync.Once +) + +func init() { + managerOnce.Do(func() { + managerInstance = NewManager() + }) +} + +func GetManager() *Manager { + return managerInstance +} + +type Manager struct { + components map[component.ComponentName]component.Component +} + +type ManagerOption func(*managerOptions) + +type managerOptions struct { + // Supported profiles + components + components []component.Component +} + +func WithComponents(components []component.Component) ManagerOption { + return func(opts *managerOptions) { + opts.components = components + } +} + +func NewManager(opts ...ManagerOption) *Manager { + manager := &Manager{ + components: make(map[component.ComponentName]component.Component), + } + + defaults := managerOptions{ + components: []component.Component{ + controllers.NewMonitoringCtrlr(), + controllers.NewLocalAuthListCtrlr(), + controllers.NewReservationCtrlr(), + controllers.NewSmartChargingCtrlr(), + controllers.NewTxCtrlr(), + controllers.NewSecurityCtrlr(), + controllers.NewClockCtrlr(), + controllers.NewDeviceDataCtrlr(), + controllers.NewAuthCtrlr(), + controllers.NewAuthCacheCtrlr(), + controllers.NewISO15118Ctrlr(), + controllers.NewDisplayCtrlr(), + controllers.NewOCPPCommCtrlr(), + controllers.NewAlignedDataCtrlr(), + controllers.NewSampledDataCtrlr(), + controllers.NewTariffCostCtrlr(), + controllers.NewCustomizationCtrlr(), + }, + } + for _, opt := range opts { + opt(&defaults) + } + + // Register components + for _, component := range defaults.components { + manager.components[component.GetName()] = component + } + + return manager +} + +func (m *Manager) UpdateVariable(name component.ComponentName, variableName variables.VariableName, attributeName string, attributeValue interface{}) error { + controller, ok := m.components[name] + if !ok { + return errors.New("controller not found") + } + + return controller.UpdateVariable(variableName, attributeName, attributeValue) +} + +func (m *Manager) GetVariable(name component.ComponentName, variableName variables.VariableName) (*variables.Variable, error) { + controller, ok := m.components[name] + if !ok { + return nil, errors.New("controller not found") + } + + return controller.GetVariable(variableName) +} diff --git a/ocpp_v201/ctrlr_manager_test.go b/ocpp_v201/ctrlr_manager_test.go new file mode 100644 index 0000000..607262c --- /dev/null +++ b/ocpp_v201/ctrlr_manager_test.go @@ -0,0 +1 @@ +package ocpp_v201 diff --git a/ocpp_v201/evse.go b/ocpp_v201/evse.go new file mode 100644 index 0000000..abf4185 --- /dev/null +++ b/ocpp_v201/evse.go @@ -0,0 +1,8 @@ +package ocpp_v201 + +import "github.com/ChargePi/ocpp-manager/ocpp_v201/component" + +type EVSE struct { + ID int + components map[component.ComponentName]component.Component +} diff --git a/ocpp_v201/variables/variables.go b/ocpp_v201/variables/variables.go new file mode 100644 index 0000000..97d320b --- /dev/null +++ b/ocpp_v201/variables/variables.go @@ -0,0 +1,170 @@ +package variables + +import "errors" + +type Mutability string + +const ( + MutabilityReadOnly Mutability = "ReadOnly" + MutabilityReadWrite Mutability = "ReadWrite" + MutabilityWriteOnly Mutability = "WriteOnly" +) + +type VariableType string + +const ( + VariableTypeString VariableType = "string" + VariableTypeInteger VariableType = "integer" + VariableTypeNumber VariableType = "number" + VariableTypeBool VariableType = "boolean" + VariableTypeOptionList VariableType = "OptionList" + VariableTypeSequenceList VariableType = "SequenceList" + VariableTypeMemberList VariableType = "MemberList" +) + +type VariableName string + +type Variable struct { + Name VariableName + // attributes are conditionally mutable. + attributes map[string]VariableAttributes + // Characteristics are read-only + Characteristics VariableCharacteristic +} + +// Validate checks if all variable attributes are valid. +func (v *Variable) Validate() bool { + for _, attributes := range v.attributes { + if attributes.Validate() == false { + return false + } + } + + // todo validate according to characteristics + + return true +} + +// UpdateVariableAttribute updates the variable attribute if it exists and if the value is valid +func (v *Variable) UpdateVariableAttribute(attribute string, value interface{}) error { + // Check if exists + existingEntry, ok := v.attributes[attribute] + if !ok { + return errors.New("Variable attribute not found: " + attribute) + } + + // Check if it we can even update it + if existingEntry.Mutability == MutabilityReadOnly { + return errors.New("Variable attribute is read-only: " + attribute) + } + + // Check if the operation is allowed + existingEntry.Value = value + if !existingEntry.Validate() { + return errors.New("invalid value") + } + + // Update the value + v.attributes[attribute] = existingEntry + return nil +} + +// GetVariableAttribute gets the variable attribute if it exists. +func (v *Variable) GetVariableAttribute(attribute string) (*VariableAttributes, error) { + // Check if exists + existingEntry, ok := v.attributes[attribute] + if !ok { + return nil, errors.New("Variable attribute not found: " + attribute) + } + + // Check if it is readable + if existingEntry.Mutability == MutabilityWriteOnly { + return nil, errors.New("Variable attribute is write-only: " + attribute) + } + + return &existingEntry, nil +} + +// GetAllAttributes returns a copy of all attributes for this variable. +func (v *Variable) GetAllAttributes() map[string]VariableAttributes { + result := make(map[string]VariableAttributes, len(v.attributes)) + for k, vAttr := range v.attributes { + result[k] = vAttr + } + return result +} + +type VariableAttributes struct { + Type VariableType + Mutability Mutability + Value interface{} +} + +// Validate validates +func (va *VariableAttributes) Validate() bool { + if va == nil { + return false + } + + switch va.Mutability { + case MutabilityReadOnly, MutabilityReadWrite, MutabilityWriteOnly: + default: + return false + } + + switch va.Type { + case VariableTypeNumber: + // Must be castable to float + _, castable := va.Value.(float64) + return castable + case VariableTypeBool: + // Must be castable to bool + _, castable := va.Value.(bool) + return castable + case VariableTypeInteger: + // Must be castable to bool + _, castable := va.Value.(int64) + return castable + case VariableTypeOptionList, VariableTypeMemberList, VariableTypeSequenceList: + _, castable := va.Value.([]interface{}) + return castable + case VariableTypeString: + _, castable := va.Value.(string) + return castable + } + + return false +} + +func (va *VariableAttributes) copy() VariableAttributes { + return *va +} + +// Update updates the variable attribute value +func (va *VariableAttributes) Update(value interface{}) error { + // Make a copy of the variable attributes to validate the new value + attrsCopy := va.copy() + attrsCopy.Value = value + if !attrsCopy.Validate() { + return errors.New("invalid value for variable attribute") + } + + va.Value = value + return nil +} + +type VariableCharacteristic struct { + DataType string + MaxLimit *int + MinLimit *int + Unit *string + ValuesList []string +} + +// NewVariable creates a new variable with the given name, type, and default value +func NewVariable(name VariableName, varType VariableType) *Variable { + return &Variable{ + Name: name, + attributes: map[string]VariableAttributes{}, + } +} diff --git a/ocpp_v201/variables/variables_test.go b/ocpp_v201/variables/variables_test.go new file mode 100644 index 0000000..810b5cc --- /dev/null +++ b/ocpp_v201/variables/variables_test.go @@ -0,0 +1,463 @@ +package variables + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/suite" +) + +type variablesTestSuite struct { + suite.Suite +} + +func (suite *variablesTestSuite) TestVariable_Validate() { + tests := []struct { + name string + variable *Variable + expected bool + }{ + { + name: "valid variable", + variable: &Variable{ + Name: "TestVariable", + attributes: map[string]VariableAttributes{ + "value": { + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "test", + }, + }, + Characteristics: VariableCharacteristic{ + DataType: "string", + }, + }, + expected: true, + }, + { + name: "variable with invalid attribute", + variable: &Variable{ + Name: "TestVariable", + attributes: map[string]VariableAttributes{ + "value": { + Type: VariableTypeString, + Mutability: "InvalidMutability", // Invalid mutability + Value: "test", + }, + }, + Characteristics: VariableCharacteristic{ + DataType: "string", + }, + }, + expected: false, + }, + { + name: "variable with empty attributes", + variable: &Variable{ + Name: "TestVariable", + attributes: map[string]VariableAttributes{}, + Characteristics: VariableCharacteristic{ + DataType: "string", + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + suite.Assert().Equal(tt.expected, tt.variable.Validate()) + }) + } +} + +func (suite *variablesTestSuite) TestVariable_GetVariableAttribute() { + var1 := &Variable{ + Name: "TestVariable", + attributes: map[string]VariableAttributes{ + "readWrite": { + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "test", + }, + "readOnly": { + Type: VariableTypeInteger, + Mutability: MutabilityReadOnly, + Value: int64(42), + }, + "writeOnly": { + Type: VariableTypeBool, + Mutability: MutabilityWriteOnly, + Value: true, + }, + }, + } + + tests := []struct { + name string + attribute string + expectError bool + errorMsg string + expectedValue interface{} + }{ + { + name: "get read-write attribute", + attribute: "readWrite", + expectError: false, + expectedValue: "test", + }, + { + name: "get read-only attribute", + attribute: "readOnly", + expectError: false, + expectedValue: int64(42), + }, + { + name: "get write-only attribute", + attribute: "writeOnly", + expectError: true, + errorMsg: "Variable attribute is write-only: writeOnly", + }, + { + name: "get non-existent attribute", + attribute: "nonExistent", + expectError: true, + errorMsg: "Variable attribute not found: nonExistent", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + attr, err := var1.GetVariableAttribute(tt.attribute) + if tt.expectError { + suite.Assert().Error(err) + suite.Assert().Equal(tt.errorMsg, err.Error()) + suite.Assert().Nil(attr) + } else { + suite.Require().NoError(err) + suite.Assert().Equal(tt.expectedValue, attr.Value) + } + }) + } +} + +func (suite *variablesTestSuite) TestVariable_UpdateVariableAttribute() { + var1 := &Variable{ + Name: "TestVariable", + attributes: map[string]VariableAttributes{ + "readWrite": { + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "old", + }, + "readOnly": { + Type: VariableTypeInteger, + Mutability: MutabilityReadOnly, + Value: int64(42), + }, + "writeOnly": { + Type: VariableTypeBool, + Mutability: MutabilityWriteOnly, + Value: false, + }, + }, + } + + tests := []struct { + name string + attribute string + value interface{} + expectError bool + errorMsg string + }{ + { + name: "update read-write attribute with valid value", + attribute: "readWrite", + value: "new", + expectError: false, + }, + { + name: "update read-only attribute", + attribute: "readOnly", + value: int64(100), + expectError: true, + errorMsg: "Variable attribute is read-only: readOnly", + }, + { + name: "update write-only attribute with valid value", + attribute: "writeOnly", + value: true, + expectError: false, + }, + { + name: "update non-existent attribute", + attribute: "nonExistent", + value: "value", + expectError: true, + errorMsg: "Variable attribute not found: nonExistent", + }, + { + name: "update with invalid value type", + attribute: "readWrite", + value: 123, // int instead of string + expectError: true, + errorMsg: "invalid value", + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + err := var1.UpdateVariableAttribute(tt.attribute, tt.value) + if tt.expectError { + suite.Assert().Error(err) + suite.Assert().Equal(tt.errorMsg, err.Error()) + } else { + suite.Require().NoError(err) + // Verify the value was updated for readable attributes + if tt.attribute == "readWrite" { + attr, err := var1.GetVariableAttribute(tt.attribute) + suite.Require().NoError(err) + suite.Assert().Equal(tt.value, attr.Value) + } + } + }) + } +} + +func (suite *variablesTestSuite) TestVariableAttributes_Validate() { + tests := []struct { + name string + attr *VariableAttributes + expected bool + }{ + { + name: "valid string attribute", + attr: &VariableAttributes{ + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "test", + }, + expected: true, + }, + { + name: "valid integer attribute", + attr: &VariableAttributes{ + Type: VariableTypeInteger, + Mutability: MutabilityReadOnly, + Value: int64(42), + }, + expected: true, + }, + { + name: "valid number attribute", + attr: &VariableAttributes{ + Type: VariableTypeNumber, + Mutability: MutabilityReadWrite, + Value: float64(3.14), + }, + expected: true, + }, + { + name: "valid boolean attribute", + attr: &VariableAttributes{ + Type: VariableTypeBool, + Mutability: MutabilityWriteOnly, + Value: true, + }, + expected: true, + }, + { + name: "valid list attributes", + attr: &VariableAttributes{ + Type: VariableTypeOptionList, + Mutability: MutabilityReadWrite, + Value: []interface{}{"item1", "item2"}, + }, + expected: true, + }, + { + name: "nil attribute", + attr: nil, + expected: false, + }, + { + name: "invalid mutability", + attr: &VariableAttributes{ + Type: VariableTypeString, + Mutability: "Invalid", + Value: "test", + }, + expected: false, + }, + { + name: "invalid type for string", + attr: &VariableAttributes{ + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: 123, // int instead of string + }, + expected: false, + }, + { + name: "invalid type for integer", + attr: &VariableAttributes{ + Type: VariableTypeInteger, + Mutability: MutabilityReadWrite, + Value: "not an int", // string instead of int64 + }, + expected: false, + }, + { + name: "invalid type for number", + attr: &VariableAttributes{ + Type: VariableTypeNumber, + Mutability: MutabilityReadWrite, + Value: "not a number", // string instead of float64 + }, + expected: false, + }, + { + name: "invalid type for boolean", + attr: &VariableAttributes{ + Type: VariableTypeBool, + Mutability: MutabilityReadWrite, + Value: "not a bool", // string instead of bool + }, + expected: false, + }, + { + name: "invalid type for list", + attr: &VariableAttributes{ + Type: VariableTypeOptionList, + Mutability: MutabilityReadWrite, + Value: "not a list", // string instead of []interface{} + }, + expected: false, + }, + { + name: "unknown variable type", + attr: &VariableAttributes{ + Type: "UnknownType", + Mutability: MutabilityReadWrite, + Value: "test", + }, + expected: false, + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + suite.Assert().Equal(tt.expected, tt.attr.Validate()) + }) + } +} + +func (suite *variablesTestSuite) TestVariableAttributes_Update() { + tests := []struct { + name string + attr *VariableAttributes + newValue interface{} + expectError bool + errorMsg string + expectedValue interface{} + }{ + { + name: "valid string update", + attr: &VariableAttributes{ + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "old", + }, + newValue: "new", + expectError: false, + expectedValue: "new", + }, + { + name: "invalid string update", + attr: &VariableAttributes{ + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "old", + }, + newValue: 123, // int instead of string + expectError: true, + errorMsg: "invalid value for variable attribute", + expectedValue: "old", // Value should remain unchanged + }, + { + name: "valid integer update", + attr: &VariableAttributes{ + Type: VariableTypeInteger, + Mutability: MutabilityReadWrite, + Value: int64(10), + }, + newValue: int64(20), + expectError: false, + expectedValue: int64(20), + }, + { + name: "invalid integer update", + attr: &VariableAttributes{ + Type: VariableTypeInteger, + Mutability: MutabilityReadWrite, + Value: int64(10), + }, + newValue: "not an int", + expectError: true, + errorMsg: "invalid value for variable attribute", + expectedValue: int64(10), // Value should remain unchanged + }, + } + + for _, tt := range tests { + suite.Run(tt.name, func() { + err := tt.attr.Update(tt.newValue) + if tt.expectError { + suite.Assert().Error(err) + suite.Assert().Equal(tt.errorMsg, err.Error()) + } else { + suite.Require().NoError(err) + } + suite.Assert().Equal(tt.expectedValue, tt.attr.Value) + }) + } +} + +func (suite *variablesTestSuite) TestVariableAttributes_Copy() { + original := &VariableAttributes{ + Type: VariableTypeString, + Mutability: MutabilityReadWrite, + Value: "original", + } + + copy := original.copy() + suite.Assert().Equal(original.Type, copy.Type) + suite.Assert().Equal(original.Mutability, copy.Mutability) + suite.Assert().Equal(original.Value, copy.Value) + + // Verify it's a deep copy + copy.Value = "modified" + suite.Assert().Equal("original", original.Value) + suite.Assert().Equal("modified", copy.Value) +} + +func (suite *variablesTestSuite) TestVariableCharacteristic() { + // Test VariableCharacteristic struct + char := VariableCharacteristic{ + DataType: "string", + MaxLimit: lo.ToPtr(100), + MinLimit: lo.ToPtr(0), + Unit: lo.ToPtr("watts"), + ValuesList: []string{"option1", "option2"}, + } + + suite.Assert().Equal("string", char.DataType) + suite.Assert().Equal(100, *char.MaxLimit) + suite.Assert().Equal(0, *char.MinLimit) + suite.Assert().Equal("watts", *char.Unit) + suite.Assert().Equal([]string{"option1", "option2"}, char.ValuesList) +} + +func TestVariables(t *testing.T) { + suite.Run(t, new(variablesTestSuite)) +}