diff --git a/example/1.6/cp/handler.go b/example/1.6/cp/handler.go index f3cabab8..a7e42de0 100644 --- a/example/1.6/cp/handler.go +++ b/example/1.6/cp/handler.go @@ -293,7 +293,7 @@ func (handler *ChargePointHandler) OnCancelReservation(request *reservation.Canc func (handler *ChargePointHandler) OnSetChargingProfile(request *smartcharging.SetChargingProfileRequest) (confirmation *smartcharging.SetChargingProfileConfirmation, err error) { //TODO: handle logic logDefault(request.GetFeatureName()).Warn("no set charging profile logic implemented yet") - return smartcharging.NewSetChargingProfileConfirmation(smartcharging.ChargingProfileStatusNotImplemented), nil + return smartcharging.NewSetChargingProfileConfirmation(smartcharging.ChargingProfileStatusNotSupported), nil } func (handler *ChargePointHandler) OnClearChargingProfile(request *smartcharging.ClearChargingProfileRequest) (confirmation *smartcharging.ClearChargingProfileConfirmation, err error) { diff --git a/example/2.0.1/csms/csms_sim.go b/example/2.0.1/csms/csms_sim.go index ae2d12e9..c6ddd050 100644 --- a/example/2.0.1/csms/csms_sim.go +++ b/example/2.0.1/csms/csms_sim.go @@ -83,7 +83,7 @@ func exampleRoutine(chargingStationID string, handler *CSMSHandler) { time.Sleep(2 * time.Second) // Reserve a connector reservationID := 42 - clientIDTokenType := types.IdTokenTypeKeyCode + clientIDTokenType := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} clientIdTag := "l33t" connectorID := 1 expiryDate := types.NewDateTime(time.Now().Add(1 * time.Hour)) diff --git a/ocpp1.6/central_system.go b/ocpp1.6/central_system.go index 2b8bec43..8a22c427 100644 --- a/ocpp1.6/central_system.go +++ b/ocpp1.6/central_system.go @@ -2,6 +2,7 @@ package ocpp16 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -360,6 +361,10 @@ func (cs *centralSystem) SetSmartChargingHandler(handler smartcharging.CentralSy cs.smartChargingHandler = handler } +func (cs *centralSystem) SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) { + cs.server.SetNewClientValidationHandler(handler) +} + func (cs *centralSystem) SetNewChargePointHandler(handler ChargePointConnectionHandler) { cs.server.SetNewClientHandler(func(chargePoint ws.Channel) { handler(chargePoint) @@ -399,23 +404,30 @@ func (cs *centralSystem) SendRequestAsync(clientId string, request ocpp.Request, } func (cs *centralSystem) Start(listenPort int, listenPath string) { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV16 + // Start server cs.server.Start(listenPort, listenPath) } func (cs *centralSystem) sendResponse(chargePointId string, confirmation ocpp.Response, err error, requestId string) { - // send error response if err != nil { - cs.error(fmt.Errorf("error handling request: %w", err)) - err := cs.server.SendError(chargePointId, requestId, ocppj.InternalError, "Error handling request", nil) + // Send error response + err = cs.server.SendError(chargePointId, requestId, ocppj.InternalError, err.Error(), nil) if err != nil { + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargePointId, requestId, err, "") + // Notify client implementation err = fmt.Errorf("error replying cp %s to request %s with 'internal error': %w", chargePointId, requestId, err) cs.error(err) } return } - if confirmation == nil { + if confirmation == nil || reflect.ValueOf(confirmation).IsNil() { err = fmt.Errorf("empty confirmation to %s for request %s", chargePointId, requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cs.server.SendError(chargePointId, requestId, ocppj.GenericError, err.Error(), nil) cs.error(err) return } @@ -423,6 +435,9 @@ func (cs *centralSystem) sendResponse(chargePointId string, confirmation ocpp.Re // send confirmation response err = cs.server.SendResponse(chargePointId, requestId, confirmation) if err != nil { + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargePointId, requestId, err, confirmation.GetFeatureName()) + // Notify client implementation err = fmt.Errorf("error replying cp %s to request %s: %w", chargePointId, requestId, err) cs.error(err) } diff --git a/ocpp1.6/charge_point.go b/ocpp1.6/charge_point.go index 90810144..c2df70f8 100644 --- a/ocpp1.6/charge_point.go +++ b/ocpp1.6/charge_point.go @@ -2,6 +2,7 @@ package ocpp16 import ( "fmt" + "reflect" "sync" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" @@ -297,19 +298,23 @@ func (cp *chargePoint) clearCallbacks(invokeCallback bool) { } func (cp *chargePoint) sendResponse(confirmation ocpp.Response, err error, requestId string) { - // send error response if err != nil { - err = cp.client.SendError(requestId, ocppj.ProtocolError, err.Error(), nil) + // Send error response + err = cp.client.SendError(requestId, ocppj.InternalError, err.Error(), nil) if err != nil { - err = fmt.Errorf("replying cs to request %s with 'protocol error': %w", requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cp.client.HandleFailedResponseError(requestId, err, "") + // Notify client implementation + err = fmt.Errorf("replying to request %s with 'internal error' failed: %w", requestId, err) cp.error(err) } - return } - if confirmation == nil { + if confirmation == nil || reflect.ValueOf(confirmation).IsNil() { err = fmt.Errorf("empty confirmation to request %s", requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cp.client.SendError(requestId, ocppj.GenericError, err.Error(), nil) cp.error(err) return } @@ -317,15 +322,21 @@ func (cp *chargePoint) sendResponse(confirmation ocpp.Response, err error, reque // send confirmation response err = cp.client.SendResponse(requestId, confirmation) if err != nil { - err = fmt.Errorf("replying cs to request %s: %w", requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cp.client.HandleFailedResponseError(requestId, err, confirmation.GetFeatureName()) + // Notify client implementation + err = fmt.Errorf("failed responding to request %s: %w", requestId, err) cp.error(err) } } func (cp *chargePoint) Start(centralSystemUrl string) error { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV16 + // Start client cp.stopC = make(chan struct{}, 1) - // Async response handler receives incoming responses/errors and triggers callbacks err := cp.client.Start(centralSystemUrl) + // Async response handler receives incoming responses/errors and triggers callbacks if err == nil { go cp.asyncCallbackHandler() } @@ -343,6 +354,10 @@ func (cp *chargePoint) Stop() { } } +func (cp *chargePoint) IsConnected() bool { + return cp.client.IsConnected() +} + func (cp *chargePoint) notImplementedError(requestId string, action string) { err := cp.client.SendError(requestId, ocppj.NotImplemented, fmt.Sprintf("no handler for action %v implemented", action), nil) if err != nil { diff --git a/ocpp1.6/core/get_configuration.go b/ocpp1.6/core/get_configuration.go index 3e15584a..75078c73 100644 --- a/ocpp1.6/core/get_configuration.go +++ b/ocpp1.6/core/get_configuration.go @@ -12,7 +12,7 @@ const GetConfigurationFeatureName = "GetConfiguration" type ConfigurationKey struct { Key string `json:"key" validate:"required,max=50"` Readonly bool `json:"readonly"` - Value *string `json:"value,omitempty" validate:"max=500"` + Value *string `json:"value,omitempty" validate:"omitempty,max=500"` } // The field definition of the GetConfiguration request payload sent by the Central System to the Charge Point. diff --git a/ocpp1.6/core/remote_start_transaction.go b/ocpp1.6/core/remote_start_transaction.go index 1fd96e51..2b008077 100644 --- a/ocpp1.6/core/remote_start_transaction.go +++ b/ocpp1.6/core/remote_start_transaction.go @@ -1,8 +1,9 @@ package core import ( - "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) // -------------------- Remote Start Transaction (CS -> CP) -------------------- @@ -19,7 +20,7 @@ type RemoteStartTransactionRequest struct { // This field definition of the RemoteStartTransaction confirmation payload, sent by the Charge Point to the Central System in response to a RemoteStartTransactionRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type RemoteStartTransactionConfirmation struct { - Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus"` + Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus16"` } // Central System can request a Charge Point to start a transaction by sending a RemoteStartTransactionRequest. diff --git a/ocpp1.6/core/remote_stop_transaction.go b/ocpp1.6/core/remote_stop_transaction.go index 97809a24..ddf94c79 100644 --- a/ocpp1.6/core/remote_stop_transaction.go +++ b/ocpp1.6/core/remote_stop_transaction.go @@ -1,8 +1,9 @@ package core import ( - "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" ) // -------------------- Remote Stop Transaction (CS -> CP) -------------------- @@ -17,7 +18,7 @@ type RemoteStopTransactionRequest struct { // This field definition of the RemoteStopTransaction confirmation payload, sent by the Charge Point to the Central System in response to a RemoteStopTransactionRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type RemoteStopTransactionConfirmation struct { - Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus"` + Status types.RemoteStartStopStatus `json:"status" validate:"required,remoteStartStopStatus16"` } // Central System can request a Charge Point to stop a transaction by sending a RemoteStopTransactionRequest to Charge Point with the identifier of the transaction. diff --git a/ocpp1.6/core/reset.go b/ocpp1.6/core/reset.go index ae035f36..7ef0770d 100644 --- a/ocpp1.6/core/reset.go +++ b/ocpp1.6/core/reset.go @@ -1,9 +1,10 @@ package core import ( + "reflect" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "gopkg.in/go-playground/validator.v9" - "reflect" ) // -------------------- Reset (CS -> CP) -------------------- @@ -45,13 +46,13 @@ func isValidResetStatus(fl validator.FieldLevel) bool { // The field definition of the Reset request payload sent by the Central System to the Charge Point. type ResetRequest struct { - Type ResetType `json:"type" validate:"required,resetType"` + Type ResetType `json:"type" validate:"required,resetType16"` } // This field definition of the Reset confirmation payload, sent by the Charge Point to the Central System in response to a ResetRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type ResetConfirmation struct { - Status ResetStatus `json:"status" validate:"required,resetStatus"` + Status ResetStatus `json:"status" validate:"required,resetStatus16"` } // The Central System SHALL send a ResetRequest for requesting a Charge Point to reset itself. @@ -62,8 +63,8 @@ type ResetConfirmation struct { // At receipt of a soft reset, the Charge Point SHALL stop ongoing transactions gracefully and send StopTransactionRequest for every ongoing transaction. // It should then restart the application software (if possible, otherwise restart the processor/controller). // At receipt of a hard reset the Charge Point SHALL restart (all) the hardware, it is not required to gracefully stop ongoing transaction. -//If possible the Charge Point sends a StopTransactionRequest for previously ongoing transactions after having restarted and having been accepted by the Central System via a BootNotificationConfirmation. -//This is a last resort solution for a not correctly functioning Charge Points, by sending a "hard" reset, (queued) information might get lost. +// If possible the Charge Point sends a StopTransactionRequest for previously ongoing transactions after having restarted and having been accepted by the Central System via a BootNotificationConfirmation. +// This is a last resort solution for a not correctly functioning Charge Points, by sending a "hard" reset, (queued) information might get lost. type ResetFeature struct{} func (f ResetFeature) GetFeatureName() string { @@ -97,6 +98,6 @@ func NewResetConfirmation(status ResetStatus) *ResetConfirmation { } func init() { - _ = types.Validate.RegisterValidation("resetType", isValidResetType) - _ = types.Validate.RegisterValidation("resetStatus", isValidResetStatus) + _ = types.Validate.RegisterValidation("resetType16", isValidResetType) + _ = types.Validate.RegisterValidation("resetStatus16", isValidResetStatus) } diff --git a/ocpp1.6/smartcharging/clear_charging_profile.go b/ocpp1.6/smartcharging/clear_charging_profile.go index e10f0a9b..b280d7fd 100644 --- a/ocpp1.6/smartcharging/clear_charging_profile.go +++ b/ocpp1.6/smartcharging/clear_charging_profile.go @@ -1,9 +1,10 @@ package smartcharging import ( + "reflect" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "gopkg.in/go-playground/validator.v9" - "reflect" ) // -------------------- Clear Charging Profile (CS -> CP) -------------------- @@ -32,14 +33,14 @@ func isValidClearChargingProfileStatus(fl validator.FieldLevel) bool { type ClearChargingProfileRequest struct { Id *int `json:"id,omitempty" validate:"omitempty"` ConnectorId *int `json:"connectorId,omitempty" validate:"omitempty,gte=0"` - ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose16"` StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gte=0"` } // This field definition of the ClearChargingProfile confirmation payload, sent by the Charge Point to the Central System in response to a ClearChargingProfileRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type ClearChargingProfileConfirmation struct { - Status ClearChargingProfileStatus `json:"status" validate:"required,chargingProfileStatus"` + Status ClearChargingProfileStatus `json:"status" validate:"required,clearChargingProfileStatus"` } // If the Central System wishes to clear some or all of the charging profiles that were previously sent the Charge Point, diff --git a/ocpp1.6/smartcharging/get_composite_schedule.go b/ocpp1.6/smartcharging/get_composite_schedule.go index 0cb174f5..dab6a128 100644 --- a/ocpp1.6/smartcharging/get_composite_schedule.go +++ b/ocpp1.6/smartcharging/get_composite_schedule.go @@ -1,9 +1,10 @@ package smartcharging import ( + "reflect" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "gopkg.in/go-playground/validator.v9" - "reflect" ) // -------------------- Get Composite Schedule (CS -> CP) -------------------- @@ -32,7 +33,7 @@ func isValidGetCompositeScheduleStatus(fl validator.FieldLevel) bool { type GetCompositeScheduleRequest struct { ConnectorId int `json:"connectorId" validate:"gte=0"` Duration int `json:"duration" validate:"gte=0"` - ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit"` + ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit16"` } // This field definition of the GetCompositeSchedule confirmation payload, sent by the Charge Point to the Central System in response to a GetCompositeScheduleRequest. diff --git a/ocpp1.6/smartcharging/set_charging_profile.go b/ocpp1.6/smartcharging/set_charging_profile.go index 347c798e..9392a82e 100644 --- a/ocpp1.6/smartcharging/set_charging_profile.go +++ b/ocpp1.6/smartcharging/set_charging_profile.go @@ -15,15 +15,15 @@ const SetChargingProfileFeatureName = "SetChargingProfile" type ChargingProfileStatus string const ( - ChargingProfileStatusAccepted ChargingProfileStatus = "Accepted" - ChargingProfileStatusRejected ChargingProfileStatus = "Rejected" - ChargingProfileStatusNotImplemented ChargingProfileStatus = "NotImplemented" + ChargingProfileStatusAccepted ChargingProfileStatus = "Accepted" + ChargingProfileStatusRejected ChargingProfileStatus = "Rejected" + ChargingProfileStatusNotSupported ChargingProfileStatus = "NotSupported" ) func isValidChargingProfileStatus(fl validator.FieldLevel) bool { status := ChargingProfileStatus(fl.Field().String()) switch status { - case ChargingProfileStatusAccepted, ChargingProfileStatusRejected, ChargingProfileStatusNotImplemented: + case ChargingProfileStatusAccepted, ChargingProfileStatusRejected, ChargingProfileStatusNotSupported: return true default: return false diff --git a/ocpp1.6/types/types.go b/ocpp1.6/types/types.go index 7843e865..a2539c45 100644 --- a/ocpp1.6/types/types.go +++ b/ocpp1.6/types/types.go @@ -2,8 +2,9 @@ package types import ( - "github.com/lorenzodonini/ocpp-go/ocppj" "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocppj" ) const ( @@ -42,7 +43,7 @@ func isValidAuthorizationStatus(fl validator.FieldLevel) bool { type IdTagInfo struct { ExpiryDate *DateTime `json:"expiryDate,omitempty" validate:"omitempty"` ParentIdTag string `json:"parentIdTag,omitempty" validate:"omitempty,max=20"` - Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus"` + Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus16"` } func NewIdTagInfo(status AuthorizationStatus) *IdTagInfo { @@ -121,7 +122,7 @@ func NewChargingSchedulePeriod(startPeriod int, limit float64) ChargingScheduleP type ChargingSchedule struct { Duration *int `json:"duration,omitempty" validate:"omitempty,gte=0"` StartSchedule *DateTime `json:"startSchedule,omitempty"` - ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"omitempty,chargingRateUnit"` + ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"omitempty,chargingRateUnit16"` ChargingSchedulePeriod []ChargingSchedulePeriod `json:"chargingSchedulePeriod" validate:"required,min=1"` MinChargingRate *float64 `json:"minChargingRate,omitempty" validate:"omitempty,gte=0"` } @@ -134,9 +135,9 @@ type ChargingProfile struct { ChargingProfileId int `json:"chargingProfileId"` TransactionId int `json:"transactionId,omitempty"` StackLevel int `json:"stackLevel" validate:"gte=0"` - ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose"` - ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind"` - RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind"` + ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose16"` + ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind16"` + RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind16"` ValidFrom *DateTime `json:"validFrom,omitempty"` ValidTo *DateTime `json:"validTo,omitempty"` ChargingSchedule *ChargingSchedule `json:"chargingSchedule" validate:"required"` @@ -300,11 +301,11 @@ func isValidUnitOfMeasure(fl validator.FieldLevel) bool { type SampledValue struct { Value string `json:"value" validate:"required"` - Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext"` + Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext16"` Format ValueFormat `json:"format,omitempty" validate:"omitempty,valueFormat"` - Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand"` - Phase Phase `json:"phase,omitempty" validate:"omitempty,phase"` - Location Location `json:"location,omitempty" validate:"omitempty,location"` + Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand16"` + Phase Phase `json:"phase,omitempty" validate:"omitempty,phase16"` + Location Location `json:"location,omitempty" validate:"omitempty,location16"` Unit UnitOfMeasure `json:"unit,omitempty" validate:"omitempty,unitOfMeasure"` } @@ -317,16 +318,16 @@ type MeterValue struct { var Validate = ocppj.Validate func init() { - _ = Validate.RegisterValidation("authorizationStatus", isValidAuthorizationStatus) - _ = Validate.RegisterValidation("chargingProfilePurpose", isValidChargingProfilePurpose) - _ = Validate.RegisterValidation("chargingProfileKind", isValidChargingProfileKind) - _ = Validate.RegisterValidation("recurrencyKind", isValidRecurrencyKind) - _ = Validate.RegisterValidation("chargingRateUnit", isValidChargingRateUnit) - _ = Validate.RegisterValidation("remoteStartStopStatus", isValidRemoteStartStopStatus) - _ = Validate.RegisterValidation("readingContext", isValidReadingContext) + _ = Validate.RegisterValidation("authorizationStatus16", isValidAuthorizationStatus) + _ = Validate.RegisterValidation("chargingProfilePurpose16", isValidChargingProfilePurpose) + _ = Validate.RegisterValidation("chargingProfileKind16", isValidChargingProfileKind) + _ = Validate.RegisterValidation("recurrencyKind16", isValidRecurrencyKind) + _ = Validate.RegisterValidation("chargingRateUnit16", isValidChargingRateUnit) + _ = Validate.RegisterValidation("remoteStartStopStatus16", isValidRemoteStartStopStatus) + _ = Validate.RegisterValidation("readingContext16", isValidReadingContext) _ = Validate.RegisterValidation("valueFormat", isValidValueFormat) - _ = Validate.RegisterValidation("measurand", isValidMeasurand) - _ = Validate.RegisterValidation("phase", isValidPhase) - _ = Validate.RegisterValidation("location", isValidLocation) + _ = Validate.RegisterValidation("measurand16", isValidMeasurand) + _ = Validate.RegisterValidation("phase16", isValidPhase) + _ = Validate.RegisterValidation("location16", isValidLocation) _ = Validate.RegisterValidation("unitOfMeasure", isValidUnitOfMeasure) } diff --git a/ocpp1.6/v16.go b/ocpp1.6/v16.go index f3806a7b..be035d71 100644 --- a/ocpp1.6/v16.go +++ b/ocpp1.6/v16.go @@ -101,6 +101,9 @@ type ChargePoint interface { // Stops the charge point routine, disconnecting it from the central system. // Any pending requests are discarded. Stop() + // Returns true if the charge point is currently connected to the central system, false otherwise. + // While automatically reconnecting to the central system, the method returns false. + IsConnected() bool // Errors returns a channel for error messages. If it doesn't exist it es created. // The channel is closed by the charge point when stopped. Errors() <-chan error @@ -233,6 +236,8 @@ type CentralSystem interface { SetRemoteTriggerHandler(handler remotetrigger.CentralSystemHandler) // Registers a handler for incoming smart charging profile messages. SetSmartChargingHandler(handler smartcharging.CentralSystemHandler) + // Registers a handler for new incoming Charging station connections. + SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) // Registers a handler for new incoming charge point connections. SetNewChargePointHandler(handler ChargePointConnectionHandler) // Registers a handler for charge point disconnections. diff --git a/ocpp1.6_test/authorize_test.go b/ocpp1.6_test/authorize_test.go index db7f4698..665b36f1 100644 --- a/ocpp1.6_test/authorize_test.go +++ b/ocpp1.6_test/authorize_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -48,7 +49,7 @@ func (suite *OcppV16TestSuite) TestAuthorizeE2EMocked() { responseRaw := []byte(responseJson) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnAuthorize", mock.AnythingOfType("string"), mock.Anything).Return(authorizeConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.AuthorizeRequest) require.True(t, ok) diff --git a/ocpp1.6_test/boot_notification_test.go b/ocpp1.6_test/boot_notification_test.go index 0a3d5962..90b33f33 100644 --- a/ocpp1.6_test/boot_notification_test.go +++ b/ocpp1.6_test/boot_notification_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Tests @@ -61,7 +62,7 @@ func (suite *OcppV16TestSuite) TestBootNotificationE2EMocked() { bootNotificationConfirmation := core.NewBootNotificationConfirmation(currentTime, interval, registrationStatus) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnBootNotification", mock.AnythingOfType("string"), mock.Anything).Return(bootNotificationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.BootNotificationRequest) require.True(t, ok) diff --git a/ocpp1.6_test/change_availability_test.go b/ocpp1.6_test/change_availability_test.go index 0726cf7b..a0192f78 100644 --- a/ocpp1.6_test/change_availability_test.go +++ b/ocpp1.6_test/change_availability_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -47,7 +48,7 @@ func (suite *OcppV16TestSuite) TestChangeAvailabilityE2EMocked() { changeAvailabilityConfirmation := core.NewChangeAvailabilityConfirmation(status) channel := NewMockWebSocket(wsId) // Setting handlers - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnChangeAvailability", mock.Anything).Return(changeAvailabilityConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.ChangeAvailabilityRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/change_configuration_test.go b/ocpp1.6_test/change_configuration_test.go index 8bb7b3a7..822de911 100644 --- a/ocpp1.6_test/change_configuration_test.go +++ b/ocpp1.6_test/change_configuration_test.go @@ -48,7 +48,7 @@ func (suite *OcppV16TestSuite) TestChangeConfigurationE2EMocked() { changeConfigurationConfirmation := core.NewChangeConfigurationConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnChangeConfiguration", mock.Anything).Return(changeConfigurationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.ChangeConfigurationRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/clear_cache_test.go b/ocpp1.6_test/clear_cache_test.go index cdec7e8c..eb9b644a 100644 --- a/ocpp1.6_test/clear_cache_test.go +++ b/ocpp1.6_test/clear_cache_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -39,7 +40,7 @@ func (suite *OcppV16TestSuite) TestClearCacheE2EMocked() { clearCacheConfirmation := core.NewClearCacheConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnClearCache", mock.Anything).Return(clearCacheConfirmation, nil) setupDefaultCentralSystemHandlers(suite, nil, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargePointHandlers(suite, coreListener, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}) diff --git a/ocpp1.6_test/data_transfer_test.go b/ocpp1.6_test/data_transfer_test.go index 60dc22c4..319b530d 100644 --- a/ocpp1.6_test/data_transfer_test.go +++ b/ocpp1.6_test/data_transfer_test.go @@ -3,6 +3,7 @@ package ocpp16_test import ( "encoding/json" "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -61,7 +62,7 @@ func (suite *OcppV16TestSuite) TestDataTransferFromChargePointE2EMocked() { dataTransferConfirmation := core.NewDataTransferConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.DataTransferRequest) require.NotNil(t, request) @@ -101,7 +102,7 @@ func (suite *OcppV16TestSuite) TestDataTransferFromCentralSystemE2EMocked() { dataTransferConfirmation := core.NewDataTransferConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnDataTransfer", mock.Anything).Return(dataTransferConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.DataTransferRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/get_configuration_test.go b/ocpp1.6_test/get_configuration_test.go index 8ee537db..5f8aac99 100644 --- a/ocpp1.6_test/get_configuration_test.go +++ b/ocpp1.6_test/get_configuration_test.go @@ -63,7 +63,7 @@ func (suite *OcppV16TestSuite) TestGetConfigurationE2EMocked() { getConfigurationConfirmation.UnknownKey = unknownKeys channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnGetConfiguration", mock.Anything).Return(getConfigurationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.GetConfigurationRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/heartbeat_test.go b/ocpp1.6_test/heartbeat_test.go index 9a647163..648f2480 100644 --- a/ocpp1.6_test/heartbeat_test.go +++ b/ocpp1.6_test/heartbeat_test.go @@ -2,11 +2,12 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -38,7 +39,7 @@ func (suite *OcppV16TestSuite) TestHeartbeatE2EMocked() { heartbeatConfirmation := core.NewHeartbeatConfirmation(currentTime) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnHeartbeat", mock.AnythingOfType("string"), mock.Anything).Return(heartbeatConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.HeartbeatRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/meter_values_test.go b/ocpp1.6_test/meter_values_test.go index 0a514f00..e5792974 100644 --- a/ocpp1.6_test/meter_values_test.go +++ b/ocpp1.6_test/meter_values_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -46,7 +47,7 @@ func (suite *OcppV16TestSuite) TestMeterValuesE2EMocked() { meterValuesConfirmation := core.NewMeterValuesConfirmation() channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnMeterValues", mock.AnythingOfType("string"), mock.Anything).Return(meterValuesConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.MeterValuesRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/ocpp16_test.go b/ocpp1.6_test/ocpp16_test.go index be21ded4..3e206edd 100644 --- a/ocpp1.6_test/ocpp16_test.go +++ b/ocpp1.6_test/ocpp16_test.go @@ -4,9 +4,12 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "reflect" "testing" + "github.com/stretchr/testify/require" + "github.com/lorenzodonini/ocpp-go/ocpp" ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6" "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" @@ -53,6 +56,7 @@ type MockWebsocketServer struct { ws.WsServer MessageHandler func(ws ws.Channel, data []byte) error NewClientHandler func(ws ws.Channel) + CheckClientHandler ws.CheckClientHandler DisconnectedClientHandler func(ws ws.Channel) } @@ -88,6 +92,10 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { + websocketServer.CheckClientHandler = handler +} + // ---------------------- MOCK WEBSOCKET CLIENT ---------------------- type MockWebsocketClient struct { mock.Mock @@ -143,6 +151,11 @@ func (websocketClient *MockWebsocketClient) Errors() <-chan error { return websocketClient.errC } +func (websocketClient *MockWebsocketClient) IsConnected() bool { + args := websocketClient.MethodCalled("IsConnected") + return args.Bool(0) +} + // Default queue capacity const queueCapacity = 10 @@ -198,49 +211,53 @@ type MockCentralSystemCoreListener struct { mock.Mock } -func (coreListener MockCentralSystemCoreListener) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnAuthorize(chargePointId string, request *core.AuthorizeRequest) (confirmation *core.AuthorizeConfirmation, err error) { args := coreListener.MethodCalled("OnAuthorize", chargePointId, request) conf := args.Get(0).(*core.AuthorizeConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnBootNotification(chargePointId string, request *core.BootNotificationRequest) (confirmation *core.BootNotificationConfirmation, err error) { args := coreListener.MethodCalled("OnBootNotification", chargePointId, request) conf := args.Get(0).(*core.BootNotificationConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnDataTransfer(chargePointId string, request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnDataTransfer(chargePointId string, request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { args := coreListener.MethodCalled("OnDataTransfer", chargePointId, request) - conf := args.Get(0).(*core.DataTransferConfirmation) - return conf, args.Error(1) + rawConf := args.Get(0) + err = args.Error(1) + if rawConf != nil { + confirmation = rawConf.(*core.DataTransferConfirmation) + } + return } -func (coreListener MockCentralSystemCoreListener) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (confirmation *core.HeartbeatConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnHeartbeat(chargePointId string, request *core.HeartbeatRequest) (confirmation *core.HeartbeatConfirmation, err error) { args := coreListener.MethodCalled("OnHeartbeat", chargePointId, request) conf := args.Get(0).(*core.HeartbeatConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnMeterValues(chargePointId string, request *core.MeterValuesRequest) (confirmation *core.MeterValuesConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnMeterValues(chargePointId string, request *core.MeterValuesRequest) (confirmation *core.MeterValuesConfirmation, err error) { args := coreListener.MethodCalled("OnMeterValues", chargePointId, request) conf := args.Get(0).(*core.MeterValuesConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnStartTransaction(chargePointId string, request *core.StartTransactionRequest) (confirmation *core.StartTransactionConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnStartTransaction(chargePointId string, request *core.StartTransactionRequest) (confirmation *core.StartTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnStartTransaction", chargePointId, request) conf := args.Get(0).(*core.StartTransactionConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnStatusNotification(chargePointId string, request *core.StatusNotificationRequest) (confirmation *core.StatusNotificationConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnStatusNotification(chargePointId string, request *core.StatusNotificationRequest) (confirmation *core.StatusNotificationConfirmation, err error) { args := coreListener.MethodCalled("OnStatusNotification", chargePointId, request) conf := args.Get(0).(*core.StatusNotificationConfirmation) return conf, args.Error(1) } -func (coreListener MockCentralSystemCoreListener) OnStopTransaction(chargePointId string, request *core.StopTransactionRequest) (confirmation *core.StopTransactionConfirmation, err error) { +func (coreListener *MockCentralSystemCoreListener) OnStopTransaction(chargePointId string, request *core.StopTransactionRequest) (confirmation *core.StopTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnStopTransaction", chargePointId, request) conf := args.Get(0).(*core.StopTransactionConfirmation) return conf, args.Error(1) @@ -251,55 +268,59 @@ type MockChargePointCoreListener struct { mock.Mock } -func (coreListener MockChargePointCoreListener) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnChangeAvailability(request *core.ChangeAvailabilityRequest) (confirmation *core.ChangeAvailabilityConfirmation, err error) { args := coreListener.MethodCalled("OnChangeAvailability", request) conf := args.Get(0).(*core.ChangeAvailabilityConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnDataTransfer(request *core.DataTransferRequest) (confirmation *core.DataTransferConfirmation, err error) { args := coreListener.MethodCalled("OnDataTransfer", request) - conf := args.Get(0).(*core.DataTransferConfirmation) - return conf, args.Error(1) + rawConf := args.Get(0) + err = args.Error(1) + if rawConf != nil { + confirmation = rawConf.(*core.DataTransferConfirmation) + } + return } -func (coreListener MockChargePointCoreListener) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { args := coreListener.MethodCalled("OnChangeConfiguration", request) conf := args.Get(0).(*core.ChangeConfigurationConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnClearCache(request *core.ClearCacheRequest) (confirmation *core.ClearCacheConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnClearCache(request *core.ClearCacheRequest) (confirmation *core.ClearCacheConfirmation, err error) { args := coreListener.MethodCalled("OnClearCache", request) conf := args.Get(0).(*core.ClearCacheConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) { args := coreListener.MethodCalled("OnGetConfiguration", request) conf := args.Get(0).(*core.GetConfigurationConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnReset(request *core.ResetRequest) (confirmation *core.ResetConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnReset(request *core.ResetRequest) (confirmation *core.ResetConfirmation, err error) { args := coreListener.MethodCalled("OnReset", request) conf := args.Get(0).(*core.ResetConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnUnlockConnector(request *core.UnlockConnectorRequest) (confirmation *core.UnlockConnectorConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnUnlockConnector(request *core.UnlockConnectorRequest) (confirmation *core.UnlockConnectorConfirmation, err error) { args := coreListener.MethodCalled("OnUnlockConnector", request) conf := args.Get(0).(*core.UnlockConnectorConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnRemoteStartTransaction(request *core.RemoteStartTransactionRequest) (confirmation *core.RemoteStartTransactionConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnRemoteStartTransaction(request *core.RemoteStartTransactionRequest) (confirmation *core.RemoteStartTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnRemoteStartTransaction", request) conf := args.Get(0).(*core.RemoteStartTransactionConfirmation) return conf, args.Error(1) } -func (coreListener MockChargePointCoreListener) OnRemoteStopTransaction(request *core.RemoteStopTransactionRequest) (confirmation *core.RemoteStopTransactionConfirmation, err error) { +func (coreListener *MockChargePointCoreListener) OnRemoteStopTransaction(request *core.RemoteStopTransactionRequest) (confirmation *core.RemoteStopTransactionConfirmation, err error) { args := coreListener.MethodCalled("OnRemoteStopTransaction", request) conf := args.Get(0).(*core.RemoteStopTransactionConfirmation) return conf, args.Error(1) @@ -542,36 +563,37 @@ func testUnsupportedRequestFromChargePoint(suite *OcppV16TestSuite, request ocpp wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on charge point, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on central system", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultChargePointHandlers(suite, nil, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: false}) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} setupDefaultCentralSystemHandlers(suite, coreListener, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}) - resultChannel := make(chan bool, 1) + resultChannel := make(chan struct{}, 1) suite.ocppjChargePoint.SetErrorHandler(func(err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) - resultChannel <- true + assert.Equal(t, map[string]interface{}{}, details) + resultChannel <- struct{}{} }) // Start suite.centralSystem.Start(8887, "somePath") err := suite.chargePoint.Start(wsUrl) - assert.Nil(t, err) - // Run request test, expecting an error + require.Nil(t, err) + // 1. Test sending an unsupported request, expecting an error err = suite.chargePoint.SendRequestAsync(request, func(confirmation ocpp.Response, err error) { t.Fail() }) - assert.Error(t, err) + require.Error(t, err) assert.Equal(t, expectedError, err.Error()) - // Run response test + // 2. Test receiving an unsupported request on the other endpoint and receiving an error + // Mark mocked request as pending, otherwise response will be ignored suite.ocppjChargePoint.RequestState.AddPendingRequest(messageId, request) err = suite.mockWsServer.MessageHandler(channel, []byte(requestJson)) assert.Nil(t, err) - result := <-resultChannel - assert.True(t, result) + _, ok := <-resultChannel + assert.True(t, ok) } func testUnsupportedRequestFromCentralSystem(suite *OcppV16TestSuite, request ocpp.Request, requestJson string, messageId string) { @@ -580,34 +602,38 @@ func testUnsupportedRequestFromCentralSystem(suite *OcppV16TestSuite, request oc wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on central system, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on charge point", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultCentralSystemHandlers(suite, nil, expectedCentralSystemOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: false}) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} setupDefaultChargePointHandlers(suite, coreListener, expectedChargePointOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}) + resultChannel := make(chan struct{}, 1) suite.ocppjCentralSystem.SetErrorHandler(func(chargePoint ws.Channel, err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) assert.Equal(t, wsId, chargePoint.ID()) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) + assert.Equal(t, map[string]interface{}{}, details) + resultChannel <- struct{}{} }) // Start suite.centralSystem.Start(8887, "somePath") err := suite.chargePoint.Start(wsUrl) - assert.Nil(t, err) - // Run request test, expecting an error + require.Nil(t, err) + // 1. Test sending an unsupported request, expecting an error err = suite.centralSystem.SendRequestAsync(wsId, request, func(confirmation ocpp.Response, err error) { t.Fail() }) - assert.Error(t, err) + require.Error(t, err) assert.Equal(t, expectedError, err.Error()) + // 2. Test receiving an unsupported request on the other endpoint and receiving an error // Mark mocked request as pending, otherwise response will be ignored suite.ocppjCentralSystem.RequestState.AddPendingRequest(wsId, messageId, request) - // Run response test err = suite.mockWsClient.MessageHandler([]byte(requestJson)) assert.Nil(t, err) + _, ok := <-resultChannel + assert.True(t, ok) } type GenericTestEntry struct { @@ -684,6 +710,16 @@ func (suite *OcppV16TestSuite) SetupTest() { ocppj.SetMessageIdGenerator(suite.messageIdGenerator.generateId) } +func (suite *OcppV16TestSuite) TestIsConnected() { + t := suite.T() + // Simulate ws connected + mockCall := suite.mockWsClient.On("IsConnected").Return(true) + assert.True(t, suite.chargePoint.IsConnected()) + // Simulate ws disconnected + mockCall.Return(false) + assert.False(t, suite.chargePoint.IsConnected()) +} + //TODO: implement generic protocol tests func TestOcpp16Protocol(t *testing.T) { diff --git a/ocpp1.6_test/proto_test.go b/ocpp1.6_test/proto_test.go new file mode 100644 index 00000000..39815663 --- /dev/null +++ b/ocpp1.6_test/proto_test.go @@ -0,0 +1,161 @@ +package ocpp16_test + +import ( + "fmt" + + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func (suite *OcppV16TestSuite) TestChargePointSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + // Setup internal communication and listeners + coreListener := &MockChargePointCoreListener{} + suite.chargePoint.SetCoreHandler(coreListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.centralSystem.Start(8887, "somePath") + err := suite.chargePoint.Start("someUrl") + require.Nil(t, err) + resultChannel := make(chan error, 1) + // Test 1: occurrence validation error + dataTransferConfirmation := core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = CustomData{Field1: "", Field2: 42} + coreListener.On("OnDataTransfer", mock.Anything).Return(dataTransferConfirmation, nil) + err = suite.centralSystem.DataTransfer(wsId, func(confirmation *core.DataTransferConfirmation, err error) { + require.Nil(t, confirmation) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result := <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferConfirmation = core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = make(chan struct{}) + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.Anything).Return(dataTransferConfirmation, nil) + err = suite.centralSystem.DataTransfer(wsId, func(confirmation *core.DataTransferConfirmation, err error) { + require.Nil(t, confirmation) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.Anything).Return(nil, nil) + err = suite.centralSystem.DataTransfer(wsId, func(confirmation *core.DataTransferConfirmation, err error) { + require.Nil(t, confirmation) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "empty confirmation to request 1234", ocppErr.Description) +} + +func (suite *OcppV16TestSuite) TestCentralSystemSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + var response *core.DataTransferConfirmation + // Setup internal communication and listeners + coreListener := &MockCentralSystemCoreListener{} + suite.centralSystem.SetCoreHandler(coreListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.centralSystem.Start(8887, "somePath") + err := suite.chargePoint.Start("someUrl") + require.Nil(t, err) + // Test 1: occurrence validation error + dataTransferConfirmation := core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = CustomData{Field1: "", Field2: 42} + coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferConfirmation, nil) + response, err = suite.chargePoint.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferConfirmation = core.NewDataTransferConfirmation(core.DataTransferStatusAccepted) + dataTransferConfirmation.Data = make(chan struct{}) + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferConfirmation, nil) + response, err = suite.chargePoint.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + coreListener.ExpectedCalls = nil + coreListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(nil, nil) + response, err = suite.chargePoint.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, fmt.Sprintf("empty confirmation to %s for request 1234", wsId), ocppErr.Description) +} + +func (suite *OcppV16TestSuite) TestErrorCodes() { + t := suite.T() + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.centralSystem.Start(8887, "somePath") + assert.Equal(t, ocppj.FormatViolationV16, ocppj.FormationViolation) +} diff --git a/ocpp1.6_test/remote_start_transaction_test.go b/ocpp1.6_test/remote_start_transaction_test.go index d19964fa..32e31425 100644 --- a/ocpp1.6_test/remote_start_transaction_test.go +++ b/ocpp1.6_test/remote_start_transaction_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" @@ -69,7 +70,7 @@ func (suite *OcppV16TestSuite) TestRemoteStartTransactionE2EMocked() { RemoteStartTransactionConfirmation := core.NewRemoteStartTransactionConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnRemoteStartTransaction", mock.Anything).Return(RemoteStartTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.RemoteStartTransactionRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/remote_stop_transaction_test.go b/ocpp1.6_test/remote_stop_transaction_test.go index d85c2ac6..7f9ec40c 100644 --- a/ocpp1.6_test/remote_stop_transaction_test.go +++ b/ocpp1.6_test/remote_stop_transaction_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" @@ -43,7 +44,7 @@ func (suite *OcppV16TestSuite) TestRemoteStopTransactionE2EMocked() { RemoteStopTransactionConfirmation := core.NewRemoteStopTransactionConfirmation(status) channel := NewMockWebSocket(wsId) - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnRemoteStopTransaction", mock.Anything).Return(RemoteStopTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.RemoteStopTransactionRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/reset_test.go b/ocpp1.6_test/reset_test.go index d2bf78bb..eaea7ed4 100644 --- a/ocpp1.6_test/reset_test.go +++ b/ocpp1.6_test/reset_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -43,7 +44,7 @@ func (suite *OcppV16TestSuite) TestResetE2EMocked() { resetConfirmation := core.NewResetConfirmation(status) channel := NewMockWebSocket(wsId) // Setting handlers - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnReset", mock.Anything).Return(resetConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.ResetRequest) require.NotNil(t, request) diff --git a/ocpp1.6_test/start_transaction_test.go b/ocpp1.6_test/start_transaction_test.go index 23494d03..5991acb5 100644 --- a/ocpp1.6_test/start_transaction_test.go +++ b/ocpp1.6_test/start_transaction_test.go @@ -2,12 +2,13 @@ package ocpp16_test import ( "fmt" + "time" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/lorenzodonini/ocpp-go/ocpp1.6/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "time" ) // Test @@ -60,7 +61,7 @@ func (suite *OcppV16TestSuite) TestStartTransactionE2EMocked() { responseRaw := []byte(responseJson) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnStartTransaction", mock.AnythingOfType("string"), mock.Anything).Return(startTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.StartTransactionRequest) require.True(t, ok) diff --git a/ocpp1.6_test/status_notification_test.go b/ocpp1.6_test/status_notification_test.go index 3b9fc6da..376aa611 100644 --- a/ocpp1.6_test/status_notification_test.go +++ b/ocpp1.6_test/status_notification_test.go @@ -57,7 +57,7 @@ func (suite *OcppV16TestSuite) TestStatusNotificationE2EMocked() { statusNotificationConfirmation := core.NewStatusNotificationConfirmation() channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnStatusNotification", mock.AnythingOfType("string"), mock.Anything).Return(statusNotificationConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.StatusNotificationRequest) require.True(t, ok) diff --git a/ocpp1.6_test/stop_transaction_test.go b/ocpp1.6_test/stop_transaction_test.go index 3cc093e4..c176875b 100644 --- a/ocpp1.6_test/stop_transaction_test.go +++ b/ocpp1.6_test/stop_transaction_test.go @@ -64,7 +64,7 @@ func (suite *OcppV16TestSuite) TestStopTransactionE2EMocked() { responseRaw := []byte(responseJson) channel := NewMockWebSocket(wsId) - coreListener := MockCentralSystemCoreListener{} + coreListener := &MockCentralSystemCoreListener{} coreListener.On("OnStopTransaction", mock.AnythingOfType("string"), mock.Anything).Return(stopTransactionConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(1).(*core.StopTransactionRequest) require.True(t, ok) diff --git a/ocpp1.6_test/unlock_connector_test.go b/ocpp1.6_test/unlock_connector_test.go index 6033e5ea..1a6173cb 100644 --- a/ocpp1.6_test/unlock_connector_test.go +++ b/ocpp1.6_test/unlock_connector_test.go @@ -2,6 +2,7 @@ package ocpp16_test import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp1.6/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -41,7 +42,7 @@ func (suite *OcppV16TestSuite) TestUnlockConnectorE2EMocked() { unlockConnectorConfirmation := core.NewUnlockConnectorConfirmation(status) channel := NewMockWebSocket(wsId) // Setting handlers - coreListener := MockChargePointCoreListener{} + coreListener := &MockChargePointCoreListener{} coreListener.On("OnUnlockConnector", mock.Anything).Return(unlockConnectorConfirmation, nil).Run(func(args mock.Arguments) { request, ok := args.Get(0).(*core.UnlockConnectorRequest) require.NotNil(t, request) diff --git a/ocpp2.0.1/charging_station.go b/ocpp2.0.1/charging_station.go index d87f51dd..8f635068 100644 --- a/ocpp2.0.1/charging_station.go +++ b/ocpp2.0.1/charging_station.go @@ -2,6 +2,7 @@ package ocpp2 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -559,32 +560,45 @@ func (cs *chargingStation) asyncCallbackHandler() { } func (cs *chargingStation) sendResponse(response ocpp.Response, err error, requestId string) { - // send error response if err != nil { - err = cs.client.SendError(requestId, ocppj.ProtocolError, err.Error(), nil) + // Send error response + err = cs.client.SendError(requestId, ocppj.InternalError, err.Error(), nil) if err != nil { - cs.error(fmt.Errorf("replying cs to request %s with 'protocol error': %w", requestId, err)) + // Error while sending an error. Will attempt to send a default error instead + cs.client.HandleFailedResponseError(requestId, err, "") + // Notify client implementation + err = fmt.Errorf("replying to request %s with 'internal error' failed: %w", requestId, err) + cs.error(err) } return } - if response == nil { + if response == nil || reflect.ValueOf(response).IsNil() { err = fmt.Errorf("empty response to request %s", requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cs.client.SendError(requestId, ocppj.GenericError, err.Error(), nil) cs.error(err) return } - // send response + // send confirmation response err = cs.client.SendResponse(requestId, response) if err != nil { - cs.error(fmt.Errorf("replying to request %s with 'protocol error': %w", requestId, err)) + // Error while sending an error. Will attempt to send a default error instead + cs.client.HandleFailedResponseError(requestId, err, response.GetFeatureName()) + // Notify client implementation + err = fmt.Errorf("failed responding to request %s: %w", requestId, err) + cs.error(err) } } func (cs *chargingStation) Start(csmsUrl string) error { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV2 + // Start client cs.stopC = make(chan struct{}, 1) - // Async response handler receives incoming responses/errors and triggers callbacks err := cs.client.Start(csmsUrl) + // Async response handler receives incoming responses/errors and triggers callbacks if err == nil { go cs.asyncCallbackHandler() } @@ -601,6 +615,10 @@ func (cs *chargingStation) Stop() { } } +func (cs *chargingStation) IsConnected() bool { + return cs.client.IsConnected() +} + func (cs *chargingStation) notImplementedError(requestId string, action string) { err := cs.client.SendError(requestId, ocppj.NotImplemented, fmt.Sprintf("no handler for action %v implemented", action), nil) if err != nil { diff --git a/ocpp2.0.1/csms.go b/ocpp2.0.1/csms.go index a612ea86..32a14af0 100644 --- a/ocpp2.0.1/csms.go +++ b/ocpp2.0.1/csms.go @@ -2,6 +2,7 @@ package ocpp2 import ( "fmt" + "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -296,8 +297,8 @@ func (cs *csms) GetDisplayMessages(clientId string, callback func(*display.GetDi return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *csms) GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), typeOfCertificate types.CertificateUse, props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error { - request := iso15118.NewGetInstalledCertificateIdsRequest(typeOfCertificate) +func (cs *csms) GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error { + request := iso15118.NewGetInstalledCertificateIdsRequest() for _, fn := range props { fn(request) } @@ -431,7 +432,7 @@ func (cs *csms) PublishFirmware(clientId string, callback func(*firmware.Publish return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *csms) RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdTokenType, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error { +func (cs *csms) RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdToken, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error { request := remotecontrol.NewRequestStartTransactionRequest(remoteStartID, IdToken) for _, fn := range props { fn(request) @@ -461,7 +462,7 @@ func (cs *csms) RequestStopTransaction(clientId string, callback func(*remotecon return cs.SendRequestAsync(clientId, request, genericCallback) } -func (cs *csms) ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdTokenType, props ...func(request *reservation.ReserveNowRequest)) error { +func (cs *csms) ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdToken, props ...func(request *reservation.ReserveNowRequest)) error { request := reservation.NewReserveNowRequest(id, expiryDateTime, idToken) for _, fn := range props { fn(request) @@ -735,6 +736,10 @@ func (cs *csms) SetDataHandler(handler data.CSMSHandler) { cs.dataHandler = handler } +func (cs *csms) SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) { + cs.server.SetNewClientValidationHandler(handler) +} + func (cs *csms) SetNewChargingStationHandler(handler ChargingStationConnectionHandler) { cs.server.SetNewClientHandler(func(chargingStation ws.Channel) { handler(chargingStation) @@ -805,27 +810,41 @@ func (cs *csms) SendRequestAsync(clientId string, request ocpp.Request, callback } func (cs *csms) Start(listenPort int, listenPath string) { + // Overriding some protocol-specific values in the lower layers globally + ocppj.FormationViolation = ocppj.FormatViolationV2 + // Start server cs.server.Start(listenPort, listenPath) } func (cs *csms) sendResponse(chargingStationID string, response ocpp.Response, err error, requestId string) { if err != nil { - err := cs.server.SendError(chargingStationID, requestId, ocppj.ProtocolError, "Couldn't generate valid confirmation", nil) + // Send error response + err = cs.server.SendError(chargingStationID, requestId, ocppj.InternalError, err.Error(), nil) if err != nil { - err = fmt.Errorf("replying cs %s to request %s with 'protocol error': %w", chargingStationID, requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargingStationID, requestId, err, "") + // Notify client implementation + err = fmt.Errorf("error replying cp %s to request %s with 'internal error': %w", chargingStationID, requestId, err) cs.error(err) } return } - if response == nil { + + if response == nil || reflect.ValueOf(response).IsNil() { err = fmt.Errorf("empty response to %s for request %s", chargingStationID, requestId) + // Sending a dummy error to server instead, then notify client implementation + _ = cs.server.SendError(chargingStationID, requestId, ocppj.GenericError, err.Error(), nil) cs.error(err) return } - // send response + + // send confirmation response err = cs.server.SendResponse(chargingStationID, requestId, response) if err != nil { - err = fmt.Errorf("replying cs %s to request %s: %w", chargingStationID, requestId, err) + // Error while sending an error. Will attempt to send a default error instead + cs.server.HandleFailedResponseError(chargingStationID, requestId, err, response.GetFeatureName()) + // Notify client implementation + err = fmt.Errorf("error replying cp %s to request %s: %w", chargingStationID, requestId, err) cs.error(err) } } diff --git a/ocpp2.0.1/firmware/firmware_status_notification.go b/ocpp2.0.1/firmware/firmware_status_notification.go index 5fbbca72..319b7d3b 100644 --- a/ocpp2.0.1/firmware/firmware_status_notification.go +++ b/ocpp2.0.1/firmware/firmware_status_notification.go @@ -16,19 +16,26 @@ const FirmwareStatusNotificationFeatureName = "FirmwareStatusNotification" type FirmwareStatus string const ( - FirmwareStatusDownloaded FirmwareStatus = "Downloaded" - FirmwareStatusDownloadFailed FirmwareStatus = "DownloadFailed" - FirmwareStatusDownloading FirmwareStatus = "Downloading" - FirmwareStatusIdle FirmwareStatus = "Idle" - FirmwareStatusInstallationFailed FirmwareStatus = "InstallationFailed" - FirmwareStatusInstalling FirmwareStatus = "Installing" - FirmwareStatusInstalled FirmwareStatus = "Installed" + FirmwareStatusDownloaded FirmwareStatus = "Downloaded" + FirmwareStatusDownloadFailed FirmwareStatus = "DownloadFailed" + FirmwareStatusDownloading FirmwareStatus = "Downloading" + FirmwareStatusDownloadScheduled FirmwareStatus = "DownloadScheduled" + FirmwareStatusDownloadPaused FirmwareStatus = "DownloadPaused" + FirmwareStatusIdle FirmwareStatus = "Idle" + FirmwareStatusInstallationFailed FirmwareStatus = "InstallationFailed" + FirmwareStatusInstalling FirmwareStatus = "Installing" + FirmwareStatusInstalled FirmwareStatus = "Installed" + FirmwareStatusInstallRebooting FirmwareStatus = "InstallRebooting" + FirmwareStatusInstallScheduled FirmwareStatus = "InstallScheduled" + FirmwareStatusInstallVerificationFailed FirmwareStatus = "InstallVerificationFailed" + FirmwareStatusInvalidSignature FirmwareStatus = "InvalidSignature" + FirmwareStatusSignatureVerified FirmwareStatus = "SignatureVerified" ) func isValidFirmwareStatus(fl validator.FieldLevel) bool { status := FirmwareStatus(fl.Field().String()) switch status { - case FirmwareStatusDownloaded, FirmwareStatusDownloadFailed, FirmwareStatusDownloading, FirmwareStatusIdle, FirmwareStatusInstallationFailed, FirmwareStatusInstalling, FirmwareStatusInstalled: + case FirmwareStatusDownloaded, FirmwareStatusDownloadFailed, FirmwareStatusDownloading, FirmwareStatusDownloadScheduled, FirmwareStatusDownloadPaused, FirmwareStatusIdle, FirmwareStatusInstallationFailed, FirmwareStatusInstalling, FirmwareStatusInstalled, FirmwareStatusInstallRebooting, FirmwareStatusInstallScheduled, FirmwareStatusInstallVerificationFailed, FirmwareStatusInvalidSignature, FirmwareStatusSignatureVerified: return true default: return false diff --git a/ocpp2.0.1/iso15118/get_installed_certificate_ids.go b/ocpp2.0.1/iso15118/get_installed_certificate_ids.go index 7f8a549a..58271d02 100644 --- a/ocpp2.0.1/iso15118/get_installed_certificate_ids.go +++ b/ocpp2.0.1/iso15118/get_installed_certificate_ids.go @@ -31,13 +31,14 @@ func isValidGetInstalledCertificateStatus(fl validator.FieldLevel) bool { // The field definition of the GetInstalledCertificateIdsRequest PDU sent by the CSMS to the Charging Station. type GetInstalledCertificateIdsRequest struct { - TypeOfCertificate types.CertificateUse `json:"typeOfCertificate" validate:"required,certificateUse"` + CertificateTypes []types.CertificateUse `json:"certificateType" validate:"omitempty,dive,certificateUse"` } // The field definition of the GetInstalledCertificateIds response payload sent by the Charging Station to the CSMS in response to a GetInstalledCertificateIdsRequest. type GetInstalledCertificateIdsResponse struct { - Status GetInstalledCertificateStatus `json:"status" validate:"required,getInstalledCertificateStatus"` - CertificateHashData []types.CertificateHashData `json:"certificateHashData,omitempty" validate:"omitempty,dive"` + Status GetInstalledCertificateStatus `json:"status" validate:"required,getInstalledCertificateStatus"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` + CertificateHashDataChain []types.CertificateHashDataChain `json:"certificateHashDataChain,omitempty" validate:"omitempty,dive"` } // To facilitate the management of the Charging Station’s installed certificates, a method of retrieving the installed certificates is provided. @@ -66,8 +67,8 @@ func (c GetInstalledCertificateIdsResponse) GetFeatureName() string { } // Creates a new GetInstalledCertificateIdsRequest, containing all required fields. There are no optional fields for this message. -func NewGetInstalledCertificateIdsRequest(typeOfCertificate types.CertificateUse) *GetInstalledCertificateIdsRequest { - return &GetInstalledCertificateIdsRequest{TypeOfCertificate: typeOfCertificate} +func NewGetInstalledCertificateIdsRequest() *GetInstalledCertificateIdsRequest { + return &GetInstalledCertificateIdsRequest{} } // Creates a new NewGetInstalledCertificateIdsResponse, containing all required fields. Additional optional fields may be set afterwards. diff --git a/ocpp2.0.1/provisioning/reset.go b/ocpp2.0.1/provisioning/reset.go index 0846241e..86d3bc66 100644 --- a/ocpp2.0.1/provisioning/reset.go +++ b/ocpp2.0.1/provisioning/reset.go @@ -52,14 +52,14 @@ func isValidResetStatus(fl validator.FieldLevel) bool { // The field definition of the Reset request payload sent by the CSMS to the Charging Station. type ResetRequest struct { - Type ResetType `json:"type" validate:"resetType"` + Type ResetType `json:"type" validate:"resetType201"` EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` } // This field definition of the Reset response payload, sent by the Charging Station to the CSMS in response to a ResetRequest. // In case the request was invalid, or couldn't be processed, an error will be sent instead. type ResetResponse struct { - Status ResetStatus `json:"status" validate:"required,resetStatus"` + Status ResetStatus `json:"status" validate:"required,resetStatus201"` StatusInfo *types.StatusInfo `json:"statusInfo" validate:"omitempty"` } @@ -103,6 +103,6 @@ func NewResetResponse(status ResetStatus) *ResetResponse { } func init() { - _ = types.Validate.RegisterValidation("resetType", isValidResetType) - _ = types.Validate.RegisterValidation("resetStatus", isValidResetStatus) + _ = types.Validate.RegisterValidation("resetType201", isValidResetType) + _ = types.Validate.RegisterValidation("resetStatus201", isValidResetStatus) } diff --git a/ocpp2.0.1/provisioning/set_variables.go b/ocpp2.0.1/provisioning/set_variables.go index 4f75be23..dfc100de 100644 --- a/ocpp2.0.1/provisioning/set_variables.go +++ b/ocpp2.0.1/provisioning/set_variables.go @@ -42,7 +42,7 @@ type SetVariableData struct { type SetVariableResult struct { AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute"` - AttributeStatus SetVariableStatus `json:"attributeStatus" validate:"required,getVariableStatus"` + AttributeStatus SetVariableStatus `json:"attributeStatus" validate:"required,setVariableStatus"` Component types.Component `json:"component" validate:"required"` Variable types.Variable `json:"variable" validate:"required"` StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` diff --git a/ocpp2.0.1/remotecontrol/request_start_transaction.go b/ocpp2.0.1/remotecontrol/request_start_transaction.go index e63d839a..aabc40f2 100644 --- a/ocpp2.0.1/remotecontrol/request_start_transaction.go +++ b/ocpp2.0.1/remotecontrol/request_start_transaction.go @@ -33,9 +33,9 @@ func isValidRequestStartStopStatus(fl validator.FieldLevel) bool { type RequestStartTransactionRequest struct { EvseID *int `json:"evseId,omitempty" validate:"omitempty,gt=0"` RemoteStartID int `json:"remoteStartId" validate:"gte=0"` - IDToken types.IdTokenType `json:"idToken" validate:"idTokenType"` + IDToken types.IdToken `json:"idToken"` ChargingProfile *types.ChargingProfile `json:"chargingProfile,omitempty"` - GroupIdToken types.IdTokenType `json:"groupIdToken,omitempty" validate:"omitempty,idTokenType"` + GroupIdToken *types.IdToken `json:"groupIdToken,omitempty" validate:"omitempty,dive"` } // This field definition of the RequestStartTransaction response payload, sent by the Charging Station to the CSMS in response to a RequestStartTransactionRequest. @@ -77,7 +77,7 @@ func (c RequestStartTransactionResponse) GetFeatureName() string { } // Creates a new RequestStartTransactionRequest, containing all required fields. Optional fields may be set afterwards. -func NewRequestStartTransactionRequest(remoteStartID int, IdToken types.IdTokenType) *RequestStartTransactionRequest { +func NewRequestStartTransactionRequest(remoteStartID int, IdToken types.IdToken) *RequestStartTransactionRequest { return &RequestStartTransactionRequest{RemoteStartID: remoteStartID, IDToken: IdToken} } diff --git a/ocpp2.0.1/reservation/reserve_now.go b/ocpp2.0.1/reservation/reserve_now.go index a5f916f9..bdac41fc 100644 --- a/ocpp2.0.1/reservation/reserve_now.go +++ b/ocpp2.0.1/reservation/reserve_now.go @@ -78,12 +78,12 @@ func isValidConnectorType(fl validator.FieldLevel) bool { // The field definition of the ReserveNow request payload sent by the CSMS to the Charging Station. type ReserveNowRequest struct { - ID int `json:"id" validate:"gte=0"` // ID of reservation - ExpiryDateTime *types.DateTime `json:"expiryDateTime" validate:"required"` - ConnectorType ConnectorType `json:"connectorType,omitempty" validate:"omitempty,connectorType"` - EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` - IdToken types.IdTokenType `json:"idToken" validate:"required,idTokenType"` - GroupIdToken types.IdTokenType `json:"groupIdToken,omitempty" validate:"omitempty,idTokenType"` + ID int `json:"id" validate:"gte=0"` // ID of reservation + ExpiryDateTime *types.DateTime `json:"expiryDateTime" validate:"required"` + ConnectorType ConnectorType `json:"connectorType,omitempty" validate:"omitempty,connectorType"` + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` + IdToken types.IdToken `json:"idToken" validate:"required,dive"` + GroupIdToken *types.IdToken `json:"groupIdToken,omitempty" validate:"omitempty,dive"` } // This field definition of the ReserveNow response payload, sent by the Charging Station to the CSMS in response to a ReserveNowRequest. @@ -125,7 +125,7 @@ func (c ReserveNowResponse) GetFeatureName() string { } // Creates a new ReserveNowRequest, containing all required fields. Optional fields may be set afterwards. -func NewReserveNowRequest(id int, expiryDateTime *types.DateTime, idToken types.IdTokenType) *ReserveNowRequest { +func NewReserveNowRequest(id int, expiryDateTime *types.DateTime, idToken types.IdToken) *ReserveNowRequest { return &ReserveNowRequest{ID: id, ExpiryDateTime: expiryDateTime, IdToken: idToken} } diff --git a/ocpp2.0.1/security/certificate_signed.go b/ocpp2.0.1/security/certificate_signed.go index 9c84d82b..7bf62a79 100644 --- a/ocpp2.0.1/security/certificate_signed.go +++ b/ocpp2.0.1/security/certificate_signed.go @@ -33,7 +33,7 @@ func isValidCertificateSignedStatus(fl validator.FieldLevel) bool { // The field definition of the CertificateSignedRequest PDU sent by the CSMS to the Charging Station. type CertificateSignedRequest struct { CertificateChain string `json:"certificateChain" validate:"required,max=10000"` - TypeOfCertificate types.CertificateSigningUse `json:"typeOfCertificate,omitempty" validate:"omitempty,certificateSigningUse"` + TypeOfCertificate types.CertificateSigningUse `json:"certificateType,omitempty" validate:"omitempty,certificateSigningUse"` } // The field definition of the CertificateSignedResponse payload sent by the Charging Station to the CSMS in response to a CertificateSignedRequest. diff --git a/ocpp2.0.1/smartcharging/clear_charging_profile.go b/ocpp2.0.1/smartcharging/clear_charging_profile.go index d12c4876..e3ce5941 100644 --- a/ocpp2.0.1/smartcharging/clear_charging_profile.go +++ b/ocpp2.0.1/smartcharging/clear_charging_profile.go @@ -22,7 +22,7 @@ const ( type ClearChargingProfileType struct { EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` - ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose201"` StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gt=0"` } diff --git a/ocpp2.0.1/smartcharging/get_charging_profiles.go b/ocpp2.0.1/smartcharging/get_charging_profiles.go index 8fa4c053..5f78e526 100644 --- a/ocpp2.0.1/smartcharging/get_charging_profiles.go +++ b/ocpp2.0.1/smartcharging/get_charging_profiles.go @@ -33,7 +33,7 @@ func isValidGetChargingProfileStatus(fl validator.FieldLevel) bool { // ChargingProfileCriterion specifies the charging profile within a GetChargingProfilesRequest. // A ChargingProfile consists of ChargingSchedule, describing the amount of power or current that can be delivered per time interval. type ChargingProfileCriterion struct { - ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose201"` StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gte=0"` ChargingProfileID []int `json:"chargingProfileId,omitempty" validate:"omitempty"` // This field SHALL NOT contain more ids than set in ChargingProfileEntries.maxLimit ChargingLimitSource []types.ChargingLimitSourceType `json:"chargingLimitSource,omitempty" validate:"omitempty,max=4,dive,chargingLimitSource"` diff --git a/ocpp2.0.1/smartcharging/get_composite_schedule.go b/ocpp2.0.1/smartcharging/get_composite_schedule.go index 08574b67..cb6eb5a7 100644 --- a/ocpp2.0.1/smartcharging/get_composite_schedule.go +++ b/ocpp2.0.1/smartcharging/get_composite_schedule.go @@ -37,7 +37,7 @@ type CompositeSchedule struct { // The field definition of the GetCompositeSchedule request payload sent by the CSMS to the Charging System. type GetCompositeScheduleRequest struct { Duration int `json:"duration" validate:"gte=0"` - ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit"` + ChargingRateUnit types.ChargingRateUnitType `json:"chargingRateUnit,omitempty" validate:"omitempty,chargingRateUnit201"` EvseID int `json:"evseId" validate:"gte=0"` } diff --git a/ocpp2.0.1/transactions/get_transaction_status.go b/ocpp2.0.1/transactions/get_transaction_status.go index 1582d6ad..d1a3f0ba 100644 --- a/ocpp2.0.1/transactions/get_transaction_status.go +++ b/ocpp2.0.1/transactions/get_transaction_status.go @@ -17,7 +17,7 @@ type GetTransactionStatusRequest struct { // In case the request was invalid, or couldn't be processed, an error will be sent instead. type GetTransactionStatusResponse struct { OngoingIndicator *bool `json:"ongoingIndicator,omitempty" validate:"omitempty"` - MessageInQueue bool `json:"messageInQueue"` + MessagesInQueue bool `json:"messagesInQueue"` } // In some scenarios a CSMS needs to know whether there are still messages for a transaction that need to be delivered. @@ -52,6 +52,6 @@ func NewGetTransactionStatusRequest() *GetTransactionStatusRequest { } // Creates a new GetTransactionStatusResponse, containing all required fields. There are no optional fields for this message. -func NewGetTransactionStatusResponse(messageInQueue bool) *GetTransactionStatusResponse { - return &GetTransactionStatusResponse{MessageInQueue: messageInQueue} +func NewGetTransactionStatusResponse(messagesInQueue bool) *GetTransactionStatusResponse { + return &GetTransactionStatusResponse{MessagesInQueue: messagesInQueue} } diff --git a/ocpp2.0.1/transactions/transaction_event.go b/ocpp2.0.1/transactions/transaction_event.go index c7620531..e759d179 100644 --- a/ocpp2.0.1/transactions/transaction_event.go +++ b/ocpp2.0.1/transactions/transaction_event.go @@ -144,9 +144,9 @@ type TransactionEventRequest struct { Offline bool `json:"offline,omitempty"` NumberOfPhasesUsed *int `json:"numberOfPhasesUsed,omitempty" validate:"omitempty,gte=0"` CableMaxCurrent *int `json:"cableMaxCurrent,omitempty"` // The maximum current of the connected cable in Ampere (A). - ReservationID *int `json:"reservationId,omitempty"` // The Id of the reservation that terminates as a result of this transaction. + ReservationID *int `json:"reservationId,omitempty"` // The ID of the reservation that terminates as a result of this transaction. TransactionInfo Transaction `json:"transactionInfo" validate:"required"` // Contains transaction specific information. - IDToken *types.IdToken `json:"idToken,omitempty" validate:"omitempty"` + IDToken *types.IdToken `json:"idToken,omitempty" validate:"omitempty,dive"` Evse *types.EVSE `json:"evse,omitempty" validate:"omitempty"` // Identifies which evse (and connector) of the Charging Station is used. MeterValue []types.MeterValue `json:"meterValue,omitempty" validate:"omitempty,dive"` // Contains the relevant meter values. } diff --git a/ocpp2.0.1/types/types.go b/ocpp2.0.1/types/types.go index 782ff6a1..0d474423 100644 --- a/ocpp2.0.1/types/types.go +++ b/ocpp2.0.1/types/types.go @@ -58,17 +58,17 @@ const ( IdTokenTypeCentral IdTokenType = "Central" IdTokenTypeEMAID IdTokenType = "eMAID" IdTokenTypeISO14443 IdTokenType = "ISO14443" + IdTokenTypeISO15693 IdTokenType = "ISO15693" IdTokenTypeKeyCode IdTokenType = "KeyCode" IdTokenTypeLocal IdTokenType = "Local" - IdTokenTypeNoAuthorization IdTokenType = "NoAuthorization" - IdTokenTypeISO15693 IdTokenType = "ISO15693" IdTokenTypeMacAddress IdTokenType = "MacAddress" + IdTokenTypeNoAuthorization IdTokenType = "NoAuthorization" ) func isValidIdTokenType(fl validator.FieldLevel) bool { tokenType := IdTokenType(fl.Field().String()) switch tokenType { - case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeNoAuthorization, IdTokenTypeISO15693, IdTokenTypeMacAddress: + case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeISO15693, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeMacAddress, IdTokenTypeNoAuthorization: return true default: return false @@ -148,7 +148,7 @@ type OCSPRequestDataType struct { HashAlgorithm HashAlgorithmType `json:"hashAlgorithm" validate:"required,hashAlgorithm"` IssuerNameHash string `json:"issuerNameHash" validate:"required,max=128"` IssuerKeyHash string `json:"issuerKeyHash" validate:"required,max=128"` - SerialNumber string `json:"serialNumber" validate:"required,max=20"` + SerialNumber string `json:"serialNumber" validate:"required,max=40"` ResponderURL string `json:"responderURL,omitempty" validate:"max=512"` } @@ -157,7 +157,14 @@ type CertificateHashData struct { HashAlgorithm HashAlgorithmType `json:"hashAlgorithm" validate:"required,hashAlgorithm"` IssuerNameHash string `json:"issuerNameHash" validate:"required,max=128"` IssuerKeyHash string `json:"issuerKeyHash" validate:"required,max=128"` - SerialNumber string `json:"serialNumber" validate:"required,max=20"` + SerialNumber string `json:"serialNumber" validate:"required,max=40"` +} + +// CertificateHashDataChain +type CertificateHashDataChain struct { + CertificateType CertificateUse `json:"certificateType" validate:"required,certificateUse"` + CertificateHashData CertificateHashData `json:"certificateHashData" validate:"required"` + ChildCertificateHashData []CertificateHashData `json:"childCertificateHashData,omitempty" validate:"omitempty,dive"` } // Certificate15118EVStatus @@ -208,13 +215,14 @@ const ( CSOSubCA1 CertificateUse = "CSOSubCA1" CSOSubCA2 CertificateUse = "CSOSubCA2" CSMSRootCertificate CertificateUse = "CSMSRootCertificate" + V2GCertificateChain CertificateUse = "V2GCertificateChain" ManufacturerRootCertificate CertificateUse = "ManufacturerRootCertificate" ) func isValidCertificateUse(fl validator.FieldLevel) bool { use := CertificateUse(fl.Field().String()) switch use { - case V2GRootCertificate, MORootCertificate, CSOSubCA1, CSOSubCA2, CSMSRootCertificate, ManufacturerRootCertificate: + case V2GRootCertificate, MORootCertificate, CSOSubCA1, CSOSubCA2, CSMSRootCertificate, V2GCertificateChain, ManufacturerRootCertificate: return true default: return false @@ -253,7 +261,7 @@ type GroupIdToken struct { } type IdTokenInfo struct { - Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus"` + Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus201"` CacheExpiryDateTime *DateTime `json:"cacheExpiryDateTime,omitempty" validate:"omitempty"` ChargingPriority int `json:"chargingPriority,omitempty" validate:"min=-9,max=9"` Language1 string `json:"language1,omitempty" validate:"max=8"` @@ -490,7 +498,7 @@ type ChargingSchedule struct { ID int `json:"id" validate:"gte=0"` // Identifies the ChargingSchedule. StartSchedule *DateTime `json:"startSchedule,omitempty" validate:"omitempty"` Duration *int `json:"duration,omitempty" validate:"omitempty,gte=0"` - ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit"` + ChargingRateUnit ChargingRateUnitType `json:"chargingRateUnit" validate:"required,chargingRateUnit201"` MinChargingRate *float64 `json:"minChargingRate,omitempty" validate:"omitempty,gte=0"` ChargingSchedulePeriod []ChargingSchedulePeriod `json:"chargingSchedulePeriod" validate:"required,min=1,max=1024"` SalesTariff *SalesTariff `json:"salesTariff,omitempty" validate:"omitempty"` // Sales tariff associated with this charging schedule. @@ -503,9 +511,9 @@ func NewChargingSchedule(id int, chargingRateUnit ChargingRateUnitType, schedule type ChargingProfile struct { ID int `json:"id" validate:"gte=0"` StackLevel int `json:"stackLevel" validate:"gte=0"` - ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose"` - ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind"` - RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind"` + ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose201"` + ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind201"` + RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind201"` ValidFrom *DateTime `json:"validFrom,omitempty"` ValidTo *DateTime `json:"validTo,omitempty"` TransactionID string `json:"transactionId,omitempty" validate:"omitempty,max=36"` @@ -537,7 +545,6 @@ func isValidRemoteStartStopStatus(fl validator.FieldLevel) bool { // Meter Value type ReadingContext string -type ValueFormat string type Measurand string type Phase string type Location string @@ -700,13 +707,13 @@ type SignedMeterValue struct { } type SampledValue struct { - Value float64 `json:"value" validate:"omitempty"` // Indicates the measured value. - Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext"` // Type of detail value: start, end or sample. Default = "Sample.Periodic" - Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand"` // Type of measurement. Default = "Energy.Active.Import.Register" - Phase Phase `json:"phase,omitempty" validate:"omitempty,phase"` // Indicates how the measured value is to be interpreted. For instance between L1 and neutral (L1-N) Please note that not all values of phase are applicable to all Measurands. When phase is absent, the measured value is interpreted as an overall value. - Location Location `json:"location,omitempty" validate:"omitempty,location"` // Indicates where the measured value has been sampled. - SignedMeterValue *SignedMeterValue `json:"signedMeterValue,omitempty" validate:"omitempty"` // Contains the MeterValueSignature with sign/encoding method information. - UnitOfMeasure *UnitOfMeasure `json:"unitOfMeasure,omitempty" validate:"omitempty"` // Represents a UnitOfMeasure including a multiplier. + Value float64 `json:"value"` // Indicates the measured value. This value is required. + Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext201"` // Type of detail value: start, end or sample. Default = "Sample.Periodic" + Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand201"` // Type of measurement. Default = "Energy.Active.Import.Register" + Phase Phase `json:"phase,omitempty" validate:"omitempty,phase201"` // Indicates how the measured value is to be interpreted. For instance between L1 and neutral (L1-N) Please note that not all values of phase are applicable to all Measurands. When phase is absent, the measured value is interpreted as an overall value. + Location Location `json:"location,omitempty" validate:"omitempty,location201"` // Indicates where the measured value has been sampled. + SignedMeterValue *SignedMeterValue `json:"signedMeterValue,omitempty" validate:"omitempty"` // Contains the MeterValueSignature with sign/encoding method information. + UnitOfMeasure *UnitOfMeasure `json:"unitOfMeasure,omitempty" validate:"omitempty"` // Represents a UnitOfMeasure including a multiplier. } type MeterValue struct { @@ -724,18 +731,18 @@ func init() { _ = Validate.RegisterValidation("genericStatus", isValidGenericStatus) _ = Validate.RegisterValidation("hashAlgorithm", isValidHashAlgorithmType) _ = Validate.RegisterValidation("messageFormat", isValidMessageFormatType) - _ = Validate.RegisterValidation("authorizationStatus", isValidAuthorizationStatus) + _ = Validate.RegisterValidation("authorizationStatus201", isValidAuthorizationStatus) _ = Validate.RegisterValidation("attribute", isValidAttribute) - _ = Validate.RegisterValidation("chargingProfilePurpose", isValidChargingProfilePurpose) - _ = Validate.RegisterValidation("chargingProfileKind", isValidChargingProfileKind) - _ = Validate.RegisterValidation("recurrencyKind", isValidRecurrencyKind) - _ = Validate.RegisterValidation("chargingRateUnit", isValidChargingRateUnit) + _ = Validate.RegisterValidation("chargingProfilePurpose201", isValidChargingProfilePurpose) + _ = Validate.RegisterValidation("chargingProfileKind201", isValidChargingProfileKind) + _ = Validate.RegisterValidation("recurrencyKind201", isValidRecurrencyKind) + _ = Validate.RegisterValidation("chargingRateUnit201", isValidChargingRateUnit) _ = Validate.RegisterValidation("chargingLimitSource", isValidChargingLimitSource) - _ = Validate.RegisterValidation("remoteStartStopStatus", isValidRemoteStartStopStatus) - _ = Validate.RegisterValidation("readingContext", isValidReadingContext) - _ = Validate.RegisterValidation("measurand", isValidMeasurand) - _ = Validate.RegisterValidation("phase", isValidPhase) - _ = Validate.RegisterValidation("location", isValidLocation) + _ = Validate.RegisterValidation("remoteStartStopStatus201", isValidRemoteStartStopStatus) + _ = Validate.RegisterValidation("readingContext201", isValidReadingContext) + _ = Validate.RegisterValidation("measurand201", isValidMeasurand) + _ = Validate.RegisterValidation("phase201", isValidPhase) + _ = Validate.RegisterValidation("location201", isValidLocation) _ = Validate.RegisterValidation("signatureMethod", isValidSignatureMethod) _ = Validate.RegisterValidation("encodingMethod", isValidEncodingMethod) _ = Validate.RegisterValidation("certificateSigningUse", isValidCertificateSigningUse) diff --git a/ocpp2.0.1/v2.go b/ocpp2.0.1/v2.go index 9a978d68..c28544ab 100644 --- a/ocpp2.0.1/v2.go +++ b/ocpp2.0.1/v2.go @@ -34,6 +34,7 @@ type ChargingStationConnection interface { TLSConnectionState() *tls.ConnectionState } +type ChargingStationValidationHandler ws.CheckClientHandler type ChargingStationConnectionHandler func(chargePoint ChargingStationConnection) // -------------------- v2.0 Charging Station -------------------- @@ -165,6 +166,9 @@ type ChargingStation interface { // Stops the charging station routine, disconnecting it from the CSMS. // Any pending requests are discarded. Stop() + // Returns true if the charging station is currently connected to the CSMS, false otherwise. + // While automatically reconnecting to the CSMS, the method returns false. + IsConnected() bool // Errors returns a channel for error messages. If it doesn't exist it es created. // The channel is closed by the charging station when stopped. Errors() <-chan error @@ -280,7 +284,7 @@ type CSMS interface { // Retrieves all messages currently configured on a charging station. GetDisplayMessages(clientId string, callback func(*display.GetDisplayMessagesResponse, error), requestId int, props ...func(*display.GetDisplayMessagesRequest)) error // Retrieves all installed certificates on a charging station. - GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), typeOfCertificate types.CertificateUse, props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error + GetInstalledCertificateIds(clientId string, callback func(*iso15118.GetInstalledCertificateIdsResponse, error), props ...func(*iso15118.GetInstalledCertificateIdsRequest)) error // Queries a charging station for version number of the Local Authorization List. GetLocalListVersion(clientId string, callback func(*localauth.GetLocalListVersionResponse, error), props ...func(*localauth.GetLocalListVersionRequest)) error // Instructs a charging station to upload a diagnostics or security logfile to the CSMS. @@ -298,11 +302,11 @@ type CSMS interface { // Publishes a firmware to a local controller, allowing charging stations to download the same firmware from the local controller directly. PublishFirmware(clientId string, callback func(*firmware.PublishFirmwareResponse, error), location string, checksum string, requestID int, props ...func(request *firmware.PublishFirmwareRequest)) error // Remotely triggers a transaction to be started on a charging station. - RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdTokenType, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error + RequestStartTransaction(clientId string, callback func(*remotecontrol.RequestStartTransactionResponse, error), remoteStartID int, IdToken types.IdToken, props ...func(request *remotecontrol.RequestStartTransactionRequest)) error // Remotely triggers an ongoing transaction to be stopped on a charging station. RequestStopTransaction(clientId string, callback func(*remotecontrol.RequestStopTransactionResponse, error), transactionID string, props ...func(request *remotecontrol.RequestStopTransactionRequest)) error // Attempts to reserve a connector for an EV, on a specific charging station. - ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdTokenType, props ...func(request *reservation.ReserveNowRequest)) error + ReserveNow(clientId string, callback func(*reservation.ReserveNowResponse, error), id int, expiryDateTime *types.DateTime, idToken types.IdToken, props ...func(request *reservation.ReserveNowRequest)) error // Instructs the Charging Station to reset itself. Reset(clientId string, callback func(*provisioning.ResetResponse, error), t provisioning.ResetType, props ...func(request *provisioning.ResetRequest)) error // Sends a local authorization list to a charging station, which can be used for the authorization of idTokens. @@ -363,6 +367,8 @@ type CSMS interface { // Registers a handler for incoming data transfer messages SetDataHandler(handler data.CSMSHandler) // Registers a handler for new incoming Charging station connections. + SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) + // Registers a handler for new incoming Charging station connections. SetNewChargingStationHandler(handler ChargingStationConnectionHandler) // Registers a handler for Charging station disconnections. SetChargingStationDisconnectedHandler(handler ChargingStationConnectionHandler) diff --git a/ocpp2.0.1_test/boot_notification_test.go b/ocpp2.0.1_test/boot_notification_test.go index f710c4d1..d342a625 100644 --- a/ocpp2.0.1_test/boot_notification_test.go +++ b/ocpp2.0.1_test/boot_notification_test.go @@ -63,7 +63,6 @@ func (suite *OcppV2TestSuite) TestBootNotificationE2EMocked() { currentTime := types.NewDateTime(time.Now()) requestJson := fmt.Sprintf(`[2,"%v","%v",{"reason":"%v","chargingStation":{"model":"%v","vendorName":"%v"}}]`, messageId, provisioning.BootNotificationFeatureName, reason, chargePointModel, chargePointVendor) responseJson := fmt.Sprintf(`[3,"%v",{"currentTime":"%v","interval":%v,"status":"%v"}]`, messageId, currentTime.FormatTimestamp(), interval, registrationStatus) - fmt.Println(responseJson) bootNotificationConfirmation := provisioning.NewBootNotificationResponse(currentTime, interval, registrationStatus) channel := NewMockWebSocket(wsId) diff --git a/ocpp2.0.1_test/certificate_signed_test.go b/ocpp2.0.1_test/certificate_signed_test.go index 573aaa15..adea66b7 100644 --- a/ocpp2.0.1_test/certificate_signed_test.go +++ b/ocpp2.0.1_test/certificate_signed_test.go @@ -46,7 +46,7 @@ func (suite *OcppV2TestSuite) TestCertificateSignedE2EMocked() { certificateChain := "someX509CertificateChain" certificateType := types.ChargingStationCert status := security.CertificateSignedStatusAccepted - requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","typeOfCertificate":"%v"}]`, + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","certificateType":"%v"}]`, messageId, security.CertificateSignedFeatureName, certificateChain, certificateType) responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v"}]`, messageId, status) certificateSignedConfirmation := security.NewCertificateSignedResponse(status) @@ -85,6 +85,6 @@ func (suite *OcppV2TestSuite) TestCertificateSignedInvalidEndpoint() { certificateType := types.ChargingStationCert certificateSignedRequest := security.NewCertificateSignedRequest(certificate) certificateSignedRequest.TypeOfCertificate = certificateType - requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","typeOfCertificate":"%v"}]`, messageId, security.CertificateSignedFeatureName, certificate, certificateType) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateChain":"%v","certificateType":"%v"}]`, messageId, security.CertificateSignedFeatureName, certificate, certificateType) testUnsupportedRequestFromChargingStation(suite, certificateSignedRequest, requestJson, messageId) } diff --git a/ocpp2.0.1_test/common_test.go b/ocpp2.0.1_test/common_test.go index 63e6bbb0..728e25f1 100644 --- a/ocpp2.0.1_test/common_test.go +++ b/ocpp2.0.1_test/common_test.go @@ -237,6 +237,7 @@ func (suite *OcppV2TestSuite) TestSampledValueValidation() { {types.SampledValue{Value: 3.14, Context: types.ReadingContextTransactionEnd}, true}, {types.SampledValue{Value: 3.14}, true}, {types.SampledValue{Value: -3.14}, true}, + {types.SampledValue{}, true}, {types.SampledValue{Value: 3.14, Context: "invalidContext"}, false}, {types.SampledValue{Value: 3.14, Measurand: "invalidMeasurand"}, false}, {types.SampledValue{Value: 3.14, Phase: "invalidPhase"}, false}, diff --git a/ocpp2.0.1_test/get_installed_certificate_ids_test.go b/ocpp2.0.1_test/get_installed_certificate_ids_test.go index c419f728..206cf0b1 100644 --- a/ocpp2.0.1_test/get_installed_certificate_ids_test.go +++ b/ocpp2.0.1_test/get_installed_certificate_ids_test.go @@ -14,14 +14,14 @@ import ( func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsRequestValidation() { t := suite.T() var testTable = []GenericTestEntry{ - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.V2GRootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.MORootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.CSOSubCA1}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.CSOSubCA2}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.CSMSRootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: types.ManufacturerRootCertificate}, true}, - {iso15118.GetInstalledCertificateIdsRequest{}, false}, - {iso15118.GetInstalledCertificateIdsRequest{TypeOfCertificate: "invalidCertificateUse"}, false}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.V2GRootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.MORootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.CSOSubCA1}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.CSOSubCA2}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.CSMSRootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{types.ManufacturerRootCertificate}}, true}, + {iso15118.GetInstalledCertificateIdsRequest{}, true}, + {iso15118.GetInstalledCertificateIdsRequest{CertificateTypes: []types.CertificateUse{"invalidCertificateUse"}}, false}, } ExecuteGenericTestTable(t, testTable) } @@ -29,13 +29,13 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsRequestValidation() func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsConfirmationValidation() { t := suite.T() var testTable = []GenericTestEntry{ - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashData: []types.CertificateHashData{{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}, true}, - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusNotFound, CertificateHashData: []types.CertificateHashData{{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}, true}, - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashData: []types.CertificateHashData{}}, true}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashDataChain: []types.CertificateHashDataChain{{CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}}, true}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusNotFound, CertificateHashDataChain: []types.CertificateHashDataChain{{CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}}, true}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashDataChain: []types.CertificateHashDataChain{}}, true}, {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted}, true}, {iso15118.GetInstalledCertificateIdsResponse{}, false}, {iso15118.GetInstalledCertificateIdsResponse{Status: "invalidGetInstalledCertificateStatus"}, false}, - {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashData: []types.CertificateHashData{{HashAlgorithm: "invalidHashAlgorithm", IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}, false}, + {iso15118.GetInstalledCertificateIdsResponse{Status: iso15118.GetInstalledCertificateStatusAccepted, CertificateHashDataChain: []types.CertificateHashDataChain{{CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: "invalidHashAlgorithm", IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}}}, false}, } ExecuteGenericTestTable(t, testTable) } @@ -46,16 +46,16 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { wsId := "test_id" messageId := defaultMessageId wsUrl := "someUrl" - certificateType := types.CSMSRootCertificate + certificateTypes := []types.CertificateUse{types.CSMSRootCertificate} status := iso15118.GetInstalledCertificateStatusAccepted - certificateHashData := []types.CertificateHashData{ - {HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}, + certificateHashDataChain := []types.CertificateHashDataChain{ + {CertificateType: types.CSMSRootCertificate, CertificateHashData: types.CertificateHashData{HashAlgorithm: types.SHA256, IssuerNameHash: "name0", IssuerKeyHash: "key0", SerialNumber: "serial0"}}, } - requestJson := fmt.Sprintf(`[2,"%v","%v",{"typeOfCertificate":"%v"}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateType) - responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","certificateHashData":[{"hashAlgorithm":"%v","issuerNameHash":"%v","issuerKeyHash":"%v","serialNumber":"%v"}]}]`, - messageId, status, certificateHashData[0].HashAlgorithm, certificateHashData[0].IssuerNameHash, certificateHashData[0].IssuerKeyHash, certificateHashData[0].SerialNumber) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateType":["%v"]}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateTypes[0]) + responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","certificateHashDataChain":[{"certificateType":"%v","certificateHashData":{"hashAlgorithm":"%v","issuerNameHash":"%v","issuerKeyHash":"%v","serialNumber":"%v"}}]}]`, + messageId, status, certificateHashDataChain[0].CertificateType, certificateHashDataChain[0].CertificateHashData.HashAlgorithm, certificateHashDataChain[0].CertificateHashData.IssuerNameHash, certificateHashDataChain[0].CertificateHashData.IssuerKeyHash, certificateHashDataChain[0].CertificateHashData.SerialNumber) getInstalledCertificateIdsConfirmation := iso15118.NewGetInstalledCertificateIdsResponse(status) - getInstalledCertificateIdsConfirmation.CertificateHashData = certificateHashData + getInstalledCertificateIdsConfirmation.CertificateHashDataChain = certificateHashDataChain channel := NewMockWebSocket(wsId) // Setting handlers handler := &MockChargingStationIso15118Handler{} @@ -63,7 +63,7 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { request, ok := args.Get(0).(*iso15118.GetInstalledCertificateIdsRequest) require.True(t, ok) require.NotNil(t, request) - assert.Equal(t, certificateType, request.TypeOfCertificate) + assert.Equal(t, certificateTypes, request.CertificateTypes) }) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}, handler) @@ -76,13 +76,15 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { require.Nil(t, err) require.NotNil(t, confirmation) assert.Equal(t, status, confirmation.Status) - require.Len(t, confirmation.CertificateHashData, len(certificateHashData)) - assert.Equal(t, certificateHashData[0].HashAlgorithm, confirmation.CertificateHashData[0].HashAlgorithm) - assert.Equal(t, certificateHashData[0].IssuerNameHash, confirmation.CertificateHashData[0].IssuerNameHash) - assert.Equal(t, certificateHashData[0].IssuerKeyHash, confirmation.CertificateHashData[0].IssuerKeyHash) - assert.Equal(t, certificateHashData[0].SerialNumber, confirmation.CertificateHashData[0].SerialNumber) + require.Len(t, confirmation.CertificateHashDataChain, len(certificateHashDataChain)) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.HashAlgorithm, confirmation.CertificateHashDataChain[0].CertificateHashData.HashAlgorithm) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.IssuerNameHash, confirmation.CertificateHashDataChain[0].CertificateHashData.IssuerNameHash) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.IssuerKeyHash, confirmation.CertificateHashDataChain[0].CertificateHashData.IssuerKeyHash) + assert.Equal(t, certificateHashDataChain[0].CertificateHashData.SerialNumber, confirmation.CertificateHashDataChain[0].CertificateHashData.SerialNumber) resultChannel <- true - }, certificateType) + }, func(request *iso15118.GetInstalledCertificateIdsRequest) { + request.CertificateTypes = certificateTypes + }) require.Nil(t, err) result := <-resultChannel assert.True(t, result) @@ -90,8 +92,9 @@ func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsE2EMocked() { func (suite *OcppV2TestSuite) TestGetInstalledCertificateIdsInvalidEndpoint() { messageId := defaultMessageId - certificateType := types.CSMSRootCertificate - GetInstalledCertificateIdsRequest := iso15118.NewGetInstalledCertificateIdsRequest(certificateType) - requestJson := fmt.Sprintf(`[2,"%v","%v",{"typeOfCertificate":"%v"}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateType) + certificateTypes := []types.CertificateUse{types.CSMSRootCertificate} + GetInstalledCertificateIdsRequest := iso15118.NewGetInstalledCertificateIdsRequest() + GetInstalledCertificateIdsRequest.CertificateTypes = certificateTypes + requestJson := fmt.Sprintf(`[2,"%v","%v",{"certificateType":["%v"]}]`, messageId, iso15118.GetInstalledCertificateIdsFeatureName, certificateTypes[0]) testUnsupportedRequestFromChargingStation(suite, GetInstalledCertificateIdsRequest, requestJson, messageId) } diff --git a/ocpp2.0.1_test/get_transaction_status_test.go b/ocpp2.0.1_test/get_transaction_status_test.go index 222bbfee..872c35a5 100644 --- a/ocpp2.0.1_test/get_transaction_status_test.go +++ b/ocpp2.0.1_test/get_transaction_status_test.go @@ -24,8 +24,8 @@ func (suite *OcppV2TestSuite) TestGetTransactionStatusRequestValidation() { func (suite *OcppV2TestSuite) TestGetTransactionStatusResponseValidation() { t := suite.T() var confirmationTable = []GenericTestEntry{ - {transactions.GetTransactionStatusResponse{OngoingIndicator: newBool(true), MessageInQueue: true}, true}, - {transactions.GetTransactionStatusResponse{MessageInQueue: true}, true}, + {transactions.GetTransactionStatusResponse{OngoingIndicator: newBool(true), MessagesInQueue: true}, true}, + {transactions.GetTransactionStatusResponse{MessagesInQueue: true}, true}, {transactions.GetTransactionStatusResponse{}, true}, } ExecuteGenericTestTable(t, confirmationTable) @@ -37,11 +37,11 @@ func (suite *OcppV2TestSuite) TestGetTransactionStatusE2EMocked() { messageId := defaultMessageId wsUrl := "someUrl" transactionID := "12345" - messageInQueue := false + messagesInQueue := false ongoingIndicator := newBool(true) requestJson := fmt.Sprintf(`[2,"%v","%v",{"transactionId":"%v"}]`, messageId, transactions.GetTransactionStatusFeatureName, transactionID) - responseJson := fmt.Sprintf(`[3,"%v",{"ongoingIndicator":%v,"messageInQueue":%v}]`, messageId, *ongoingIndicator, messageInQueue) - getTransactionStatusResponse := transactions.NewGetTransactionStatusResponse(messageInQueue) + responseJson := fmt.Sprintf(`[3,"%v",{"ongoingIndicator":%v,"messagesInQueue":%v}]`, messageId, *ongoingIndicator, messagesInQueue) + getTransactionStatusResponse := transactions.NewGetTransactionStatusResponse(messagesInQueue) getTransactionStatusResponse.OngoingIndicator = ongoingIndicator channel := NewMockWebSocket(wsId) @@ -62,7 +62,7 @@ func (suite *OcppV2TestSuite) TestGetTransactionStatusE2EMocked() { err = suite.csms.GetTransactionStatus(wsId, func(response *transactions.GetTransactionStatusResponse, err error) { require.Nil(t, err) require.NotNil(t, response) - assert.Equal(t, messageInQueue, response.MessageInQueue) + assert.Equal(t, messagesInQueue, response.MessagesInQueue) require.NotNil(t, response.OngoingIndicator) require.Equal(t, *ongoingIndicator, *response.OngoingIndicator) resultChannel <- true diff --git a/ocpp2.0.1_test/ocpp2_test.go b/ocpp2.0.1_test/ocpp2_test.go index 9e144378..dc1cc53d 100644 --- a/ocpp2.0.1_test/ocpp2_test.go +++ b/ocpp2.0.1_test/ocpp2_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "reflect" "testing" @@ -68,6 +69,7 @@ type MockWebsocketServer struct { ws.WsServer MessageHandler func(ws ws.Channel, data []byte) error NewClientHandler func(ws ws.Channel) + CheckClientHandler ws.CheckClientHandler DisconnectedClientHandler func(ws ws.Channel) } @@ -103,6 +105,10 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { + websocketServer.CheckClientHandler = handler +} + // ---------------------- MOCK WEBSOCKET CLIENT ---------------------- type MockWebsocketClient struct { @@ -159,6 +165,11 @@ func (websocketClient *MockWebsocketClient) Errors() <-chan error { return websocketClient.errC } +func (websocketClient *MockWebsocketClient) IsConnected() bool { + args := websocketClient.MethodCalled("IsConnected") + return args.Bool(0) +} + // Default queue capacity const queueCapacity = 10 @@ -389,10 +400,14 @@ type MockChargingStationDataHandler struct { mock.Mock } -func (handler *MockChargingStationDataHandler) OnDataTransfer(request *data.DataTransferRequest) (confirmation *data.DataTransferResponse, err error) { +func (handler *MockChargingStationDataHandler) OnDataTransfer(request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) { args := handler.MethodCalled("OnDataTransfer", request) - conf := args.Get(0).(*data.DataTransferResponse) - return conf, args.Error(1) + rawResp := args.Get(0) + err = args.Error(1) + if rawResp != nil { + response = rawResp.(*data.DataTransferResponse) + } + return } // ---------------------- MOCK CSMS DATA HANDLER ---------------------- @@ -401,10 +416,14 @@ type MockCSMSDataHandler struct { mock.Mock } -func (handler *MockCSMSDataHandler) OnDataTransfer(chargingStationID string, request *data.DataTransferRequest) (confirmation *data.DataTransferResponse, err error) { +func (handler *MockCSMSDataHandler) OnDataTransfer(chargingStationID string, request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) { args := handler.MethodCalled("OnDataTransfer", chargingStationID, request) - conf := args.Get(0).(*data.DataTransferResponse) - return conf, args.Error(1) + rawResp := args.Get(0) + err = args.Error(1) + if rawResp != nil { + response = rawResp.(*data.DataTransferResponse) + } + return } // ---------------------- MOCK CS DIAGNOSTICS HANDLER ---------------------- @@ -977,7 +996,7 @@ func testUnsupportedRequestFromChargingStation(suite *OcppV2TestSuite, request o wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on charging station, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on CSMS", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: false}) @@ -987,20 +1006,21 @@ func testUnsupportedRequestFromChargingStation(suite *OcppV2TestSuite, request o assert.Equal(t, messageId, err.MessageId) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) + assert.Equal(t, map[string]interface{}{}, details) resultChannel <- true }) // Start suite.csms.Start(8887, "somePath") err := suite.chargingStation.Start(wsUrl) require.Nil(t, err) - // Run request test + // 1. Test sending an unsupported request, expecting an error err = suite.chargingStation.SendRequestAsync(request, func(confirmation ocpp.Response, err error) { t.Fail() }) require.Error(t, err) assert.Equal(t, expectedError, err.Error()) - // Run response test + // 2. Test receiving an unsupported request on the other endpoint and receiving an error + // Mark mocked request as pending, otherwise response will be ignored suite.ocppjClient.RequestState.AddPendingRequest(messageId, request) err = suite.mockWsServer.MessageHandler(channel, []byte(requestJson)) require.Nil(t, err) @@ -1014,33 +1034,38 @@ func testUnsupportedRequestFromCentralSystem(suite *OcppV2TestSuite, request ocp wsUrl := "someUrl" expectedError := fmt.Sprintf("unsupported action %v on CSMS, cannot send request", request.GetFeatureName()) errorDescription := fmt.Sprintf("unsupported action %v on charging station", request.GetFeatureName()) - errorJson := fmt.Sprintf(`[4,"%v","%v","%v",null]`, messageId, ocppj.NotSupported, errorDescription) + errorJson := fmt.Sprintf(`[4,"%v","%v","%v",{}]`, messageId, ocppj.NotSupported, errorDescription) channel := NewMockWebSocket(wsId) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: false}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(errorJson), forwardWrittenMessage: true}, handlers...) + resultChannel := make(chan struct{}, 1) suite.ocppjServer.SetErrorHandler(func(channel ws.Channel, err *ocpp.Error, details interface{}) { assert.Equal(t, messageId, err.MessageId) assert.Equal(t, wsId, channel.ID()) assert.Equal(t, ocppj.NotSupported, err.Code) assert.Equal(t, errorDescription, err.Description) - assert.Nil(t, details) + assert.Equal(t, map[string]interface{}{}, details) + resultChannel <- struct{}{} }) // Start suite.csms.Start(8887, "somePath") err := suite.chargingStation.Start(wsUrl) require.Nil(t, err) - // Run request test, expecting an error + // 1. Test sending an unsupported request, expecting an error err = suite.csms.SendRequestAsync(wsId, request, func(response ocpp.Response, err error) { t.Fail() }) require.Error(t, err) assert.Equal(t, expectedError, err.Error()) + // 2. Test receiving an unsupported request on the other endpoint and receiving an error // Mark mocked request as pending, otherwise response will be ignored suite.ocppjServer.RequestState.AddPendingRequest(wsId, messageId, request) // Run response test err = suite.mockWsClient.MessageHandler([]byte(requestJson)) assert.Nil(t, err) + _, ok := <-resultChannel + assert.True(t, ok) } type GenericTestEntry struct { @@ -1118,6 +1143,16 @@ func (suite *OcppV2TestSuite) SetupTest() { ocppj.SetMessageIdGenerator(suite.messageIdGenerator.generateId) } +func (suite *OcppV2TestSuite) TestIsConnected() { + t := suite.T() + // Simulate ws connected + mockCall := suite.mockWsClient.On("IsConnected").Return(true) + assert.True(t, suite.chargingStation.IsConnected()) + // Simulate ws disconnected + mockCall.Return(false) + assert.False(t, suite.chargingStation.IsConnected()) +} + //TODO: implement generic protocol tests func TestOcpp2Protocol(t *testing.T) { diff --git a/ocpp2.0.1_test/proto_test.go b/ocpp2.0.1_test/proto_test.go new file mode 100644 index 00000000..95b3af2d --- /dev/null +++ b/ocpp2.0.1_test/proto_test.go @@ -0,0 +1,166 @@ +package ocpp2_test + +import ( + "fmt" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/data" + + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func (suite *OcppV2TestSuite) TestChargePointSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + // Setup internal communication and listeners + dataListener := &MockChargingStationDataHandler{} + suite.chargingStation.SetDataHandler(dataListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.csms.Start(8887, "somePath") + err := suite.chargingStation.Start("someUrl") + require.Nil(t, err) + resultChannel := make(chan error, 1) + // Test 1: occurrence validation error + dataTransferResponse := data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = struct { + Field1 string `validate:"required"` + }{Field1: ""} + dataListener.On("OnDataTransfer", mock.Anything).Return(dataTransferResponse, nil) + err = suite.csms.DataTransfer(wsId, func(response *data.DataTransferResponse, err error) { + require.Nil(t, response) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result := <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferResponse = data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = make(chan struct{}) + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.Anything).Return(dataTransferResponse, nil) + err = suite.csms.DataTransfer(wsId, func(response *data.DataTransferResponse, err error) { + require.Nil(t, response) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.Anything).Return(nil, nil) + err = suite.csms.DataTransfer(wsId, func(response *data.DataTransferResponse, err error) { + require.Nil(t, response) + require.Error(t, err) + resultChannel <- err + }, "vendor1") + require.Nil(t, err) + result = <-resultChannel + require.IsType(t, &ocpp.Error{}, result) + ocppErr = result.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "empty response to request 1234", ocppErr.Description) +} + +func (suite *OcppV2TestSuite) TestCentralSystemSendResponseError() { + t := suite.T() + wsId := "test_id" + channel := NewMockWebSocket(wsId) + var ocppErr *ocpp.Error + var response *data.DataTransferResponse + // Setup internal communication and listeners + dataListener := &MockCSMSDataHandler{} + suite.csms.SetDataHandler(dataListener) + suite.mockWsClient.On("Start", mock.AnythingOfType("string")).Return(nil).Run(func(args mock.Arguments) { + // Notify server of incoming connection + suite.mockWsServer.NewClientHandler(channel) + }) + suite.mockWsClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(0) + bytes := rawMsg.([]byte) + err := suite.mockWsServer.MessageHandler(channel, bytes) + assert.Nil(t, err) + }) + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockWsServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + rawMsg := args.Get(1) + bytes := rawMsg.([]byte) + err := suite.mockWsClient.MessageHandler(bytes) + assert.NoError(t, err) + }) + // Run Tests + suite.csms.Start(8887, "somePath") + err := suite.chargingStation.Start("someUrl") + require.Nil(t, err) + // Test 1: occurrence validation error + dataTransferResponse := data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = struct { + Field1 string `validate:"required"` + }{Field1: ""} + dataListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferResponse, nil) + response, err = suite.chargingStation.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.OccurrenceConstraintViolation, ocppErr.Code) + assert.Equal(t, "Field CallResult.Payload.Data.Field1 required but not found for feature DataTransfer", ocppErr.Description) + // Test 2: marshaling error + dataTransferResponse = data.NewDataTransferResponse(data.DataTransferStatusAccepted) + dataTransferResponse.Data = make(chan struct{}) + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(dataTransferResponse, nil) + response, err = suite.chargingStation.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, "json: unsupported type: chan struct {}", ocppErr.Description) + // Test 3: no results in callback + dataListener.ExpectedCalls = nil + dataListener.On("OnDataTransfer", mock.AnythingOfType("string"), mock.Anything).Return(nil, nil) + response, err = suite.chargingStation.DataTransfer("vendor1") + require.Nil(t, response) + require.Error(t, err) + require.IsType(t, &ocpp.Error{}, err) + ocppErr = err.(*ocpp.Error) + assert.Equal(t, ocppj.GenericError, ocppErr.Code) + assert.Equal(t, fmt.Sprintf("empty response to %s for request 1234", wsId), ocppErr.Description) +} + +func (suite *OcppV2TestSuite) TestErrorCodes() { + t := suite.T() + suite.mockWsServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.csms.Start(8887, "somePath") + assert.Equal(t, ocppj.FormatViolationV2, ocppj.FormationViolation) +} diff --git a/ocpp2.0.1_test/request_start_transaction_test.go b/ocpp2.0.1_test/request_start_transaction_test.go index 13388a2f..c778a68c 100644 --- a/ocpp2.0.1_test/request_start_transaction_test.go +++ b/ocpp2.0.1_test/request_start_transaction_test.go @@ -32,17 +32,17 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionRequestValidation() { }, } var requestTable = []GenericTestEntry{ - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, true}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile}, true}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode}, true}, - {remotecontrol.RequestStartTransactionRequest{RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode}, true}, - {remotecontrol.RequestStartTransactionRequest{IDToken: types.IdTokenTypeKeyCode}, true}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, true}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile}, true}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {remotecontrol.RequestStartTransactionRequest{RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {remotecontrol.RequestStartTransactionRequest{IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, {remotecontrol.RequestStartTransactionRequest{}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(0), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: -1, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: "invalidIdToken", ChargingProfile: &chargingProfile, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &types.ChargingProfile{}, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdTokenTypeKeyCode, ChargingProfile: &chargingProfile, GroupIdToken: "invalidGroupIdToken"}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(0), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: -1, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: "invalidIdToken"}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &types.ChargingProfile{}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {remotecontrol.RequestStartTransactionRequest{EvseID: newInt(1), RemoteStartID: 42, IDToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, ChargingProfile: &chargingProfile, GroupIdToken: &types.IdToken{IdToken: "1234", Type: "invalidGroupIdToken"}}, false}, } ExecuteGenericTestTable(t, requestTable) } @@ -69,7 +69,7 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { wsUrl := "someUrl" evseId := newInt(1) remoteStartID := 42 - idToken := types.IdTokenTypeKeyCode + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} schedule := []types.ChargingSchedule{ { ID: 1, @@ -89,12 +89,12 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { ChargingProfileKind: types.ChargingProfileKindAbsolute, ChargingSchedule: schedule, } - groupIdToken := types.IdTokenTypeISO15693 + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} status := remotecontrol.RequestStartStopStatusAccepted transactionId := "12345" statusInfo := types.StatusInfo{ReasonCode: "200"} - requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":"%v","chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"id":%v,"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":"%v"}]`, - messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ID, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":{"idToken":"%s","type":"%s"},"chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"id":%v,"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken.IdToken, idToken.Type, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ID, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken.IdToken, groupIdToken.Type) responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","transactionId":"%v","statusInfo":{"reasonCode":"%v"}}]`, messageId, status, transactionId, statusInfo.ReasonCode) requestStartTransactionResponse := remotecontrol.NewRequestStartTransactionResponse(status) @@ -108,7 +108,8 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { require.True(t, ok) assert.Equal(t, *evseId, *request.EvseID) assert.Equal(t, remoteStartID, request.RemoteStartID) - assert.Equal(t, idToken, request.IDToken) + assert.Equal(t, idToken.IdToken, request.IDToken.IdToken) + assert.Equal(t, idToken.Type, request.IDToken.Type) assert.Equal(t, chargingProfile.ID, request.ChargingProfile.ID) assert.Equal(t, chargingProfile.ChargingProfilePurpose, request.ChargingProfile.ChargingProfilePurpose) assert.Equal(t, chargingProfile.ChargingProfileKind, request.ChargingProfile.ChargingProfileKind) @@ -119,6 +120,9 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { require.Len(t, s.ChargingSchedulePeriod, len(chargingProfile.ChargingSchedule[0].ChargingSchedulePeriod)) assert.Equal(t, chargingProfile.ChargingSchedule[0].ChargingSchedulePeriod[0].Limit, s.ChargingSchedulePeriod[0].Limit) assert.Equal(t, chargingProfile.ChargingSchedule[0].ChargingSchedulePeriod[0].StartPeriod, s.ChargingSchedulePeriod[0].StartPeriod) + require.NotNil(t, request.GroupIdToken) + assert.Equal(t, groupIdToken.IdToken, request.GroupIdToken.IdToken) + assert.Equal(t, groupIdToken.Type, request.GroupIdToken.Type) }) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}, handler) @@ -137,7 +141,7 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionE2EMocked() { }, remoteStartID, idToken, func(request *remotecontrol.RequestStartTransactionRequest) { request.EvseID = evseId request.ChargingProfile = &chargingProfile - request.GroupIdToken = groupIdToken + request.GroupIdToken = &groupIdToken }) require.Nil(t, err) result := <-resultChannel @@ -148,7 +152,7 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionInvalidEndpoint() { messageId := defaultMessageId evseId := newInt(1) remoteStartID := 42 - idToken := types.IdTokenTypeKeyCode + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} schedule := []types.ChargingSchedule{ { ChargingRateUnit: types.ChargingRateUnitAmperes, @@ -167,15 +171,15 @@ func (suite *OcppV2TestSuite) TestRequestStartTransactionInvalidEndpoint() { ChargingProfileKind: types.ChargingProfileKindAbsolute, ChargingSchedule: schedule, } - groupIdToken := types.IdTokenTypeISO15693 + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} request := remotecontrol.RequestStartTransactionRequest{ EvseID: evseId, RemoteStartID: remoteStartID, IDToken: idToken, ChargingProfile: &chargingProfile, - GroupIdToken: groupIdToken, + GroupIdToken: &groupIdToken, } - requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":"%v","chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":"%v"}]`, - messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"evseId":%v,"remoteStartId":%v,"idToken":{"idToken":"%s","type":"%s"},"chargingProfile":{"id":%v,"stackLevel":%v,"chargingProfilePurpose":"%v","chargingProfileKind":"%v","chargingSchedule":[{"id":%v,"chargingRateUnit":"%v","chargingSchedulePeriod":[{"startPeriod":%v,"limit":%v}]}]},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, remotecontrol.RequestStartTransactionFeatureName, *evseId, remoteStartID, idToken.IdToken, idToken.Type, chargingProfile.ID, chargingProfile.StackLevel, chargingProfile.ChargingProfilePurpose, chargingProfile.ChargingProfileKind, schedule[0].ID, schedule[0].ChargingRateUnit, schedule[0].ChargingSchedulePeriod[0].StartPeriod, schedule[0].ChargingSchedulePeriod[0].Limit, groupIdToken.IdToken, groupIdToken.Type) testUnsupportedRequestFromChargingStation(suite, request, requestJson, messageId) } diff --git a/ocpp2.0.1_test/reserve_now_test.go b/ocpp2.0.1_test/reserve_now_test.go index fd82e82f..2ab17b81 100644 --- a/ocpp2.0.1_test/reserve_now_test.go +++ b/ocpp2.0.1_test/reserve_now_test.go @@ -16,19 +16,19 @@ import ( func (suite *OcppV2TestSuite) TestReserveNowRequestValidation() { t := suite.T() var requestTable = []GenericTestEntry{ - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, true}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode}, true}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, IdToken: types.IdTokenTypeKeyCode}, true}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdTokenTypeKeyCode}, true}, - {reservation.ReserveNowRequest{ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdTokenTypeKeyCode}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, + {reservation.ReserveNowRequest{ExpiryDateTime: types.NewDateTime(time.Now()), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, true}, {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now())}, false}, - {reservation.ReserveNowRequest{ID: 42, IdToken: types.IdTokenTypeKeyCode}, false}, + {reservation.ReserveNowRequest{ID: 42, IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}}, false}, {reservation.ReserveNowRequest{}, false}, - {reservation.ReserveNowRequest{ID: -1, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: "invalidConnectorType", EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(-1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: "invalidIdToken", GroupIdToken: types.IdTokenTypeISO15693}, false}, - {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdTokenTypeKeyCode, GroupIdToken: "invalidIdToken"}, false}, + {reservation.ReserveNowRequest{ID: -1, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: "invalidConnectorType", EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(-1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: "invalidIdToken"}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693}}, false}, + {reservation.ReserveNowRequest{ID: 42, ExpiryDateTime: types.NewDateTime(time.Now()), ConnectorType: reservation.ConnectorTypeCCS1, EvseID: newInt(1), IdToken: types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode}, GroupIdToken: &types.IdToken{IdToken: "1234", Type: "invalidIdToken"}}, false}, } ExecuteGenericTestTable(t, requestTable) } @@ -54,12 +54,12 @@ func (suite *OcppV2TestSuite) TestReserveNowE2EMocked() { expiryDateTime := types.NewDateTime(time.Now()) connectorType := reservation.ConnectorTypeCCS1 evseID := newInt(1) - idToken := types.IdTokenTypeKeyCode - groupIdToken := types.IdTokenTypeISO15693 + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} status := reservation.ReserveNowStatusAccepted statusInfo := types.StatusInfo{ReasonCode: "200"} - requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":"%v","groupIdToken":"%v"}]`, - messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":{"idToken":"%s","type":"%s"},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken.IdToken, idToken.Type, groupIdToken.IdToken, groupIdToken.Type) responseJson := fmt.Sprintf(`[3,"%v",{"status":"%v","statusInfo":{"reasonCode":"%v"}}]`, messageId, status, statusInfo.ReasonCode) reserveNowResponse := reservation.NewReserveNowResponse(status) @@ -74,8 +74,11 @@ func (suite *OcppV2TestSuite) TestReserveNowE2EMocked() { assert.Equal(t, expiryDateTime.FormatTimestamp(), request.ExpiryDateTime.FormatTimestamp()) assert.Equal(t, connectorType, request.ConnectorType) assert.Equal(t, *evseID, *request.EvseID) - assert.Equal(t, idToken, request.IdToken) - assert.Equal(t, groupIdToken, request.GroupIdToken) + assert.Equal(t, idToken.IdToken, request.IdToken.IdToken) + assert.Equal(t, idToken.Type, request.IdToken.Type) + require.NotNil(t, request.GroupIdToken) + assert.Equal(t, groupIdToken.IdToken, request.GroupIdToken.IdToken) + assert.Equal(t, groupIdToken.Type, request.GroupIdToken.Type) }) setupDefaultCSMSHandlers(suite, expectedCSMSOptions{clientId: wsId, rawWrittenMessage: []byte(requestJson), forwardWrittenMessage: true}) setupDefaultChargingStationHandlers(suite, expectedChargingStationOptions{serverUrl: wsUrl, clientId: wsId, createChannelOnStart: true, channel: channel, rawWrittenMessage: []byte(responseJson), forwardWrittenMessage: true}, handler) @@ -93,7 +96,7 @@ func (suite *OcppV2TestSuite) TestReserveNowE2EMocked() { }, id, expiryDateTime, idToken, func(request *reservation.ReserveNowRequest) { request.ConnectorType = connectorType request.EvseID = evseID - request.GroupIdToken = groupIdToken + request.GroupIdToken = &groupIdToken }) require.Nil(t, err) result := <-resultChannel @@ -106,17 +109,17 @@ func (suite *OcppV2TestSuite) TestReserveNowInvalidEndpoint() { expiryDateTime := types.NewDateTime(time.Now()) connectorType := reservation.ConnectorTypeCCS1 evseID := newInt(1) - idToken := types.IdTokenTypeKeyCode - groupIdToken := types.IdTokenTypeISO15693 + idToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} + groupIdToken := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeISO15693} reserveNowRequest := reservation.ReserveNowRequest{ ID: id, ExpiryDateTime: expiryDateTime, ConnectorType: connectorType, EvseID: evseID, IdToken: idToken, - GroupIdToken: groupIdToken, + GroupIdToken: &groupIdToken, } - requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":"%v","groupIdToken":"%v"}]`, - messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken, groupIdToken) + requestJson := fmt.Sprintf(`[2,"%v","%v",{"id":%v,"expiryDateTime":"%v","connectorType":"%v","evseId":%v,"idToken":{"idToken":"%s","type":"%s"},"groupIdToken":{"idToken":"%s","type":"%s"}}]`, + messageId, reservation.ReserveNowFeatureName, id, expiryDateTime.FormatTimestamp(), connectorType, *evseID, idToken.IdToken, idToken.Type, groupIdToken.IdToken, groupIdToken.Type) testUnsupportedRequestFromChargingStation(suite, reserveNowRequest, requestJson, messageId) } diff --git a/ocppj/central_system_test.go b/ocppj/central_system_test.go index 482d3c38..5d6ec7ed 100644 --- a/ocppj/central_system_test.go +++ b/ocppj/central_system_test.go @@ -92,7 +92,7 @@ func (suite *OcppJTestSuite) TestCentralSystemSendRequestNoValidation() { assert.Nil(suite.T(), err) } -func (suite *OcppJTestSuite) TestServerSendInvalidJsonRequest() { +func (suite *OcppJTestSuite) TestCentralSystemSendInvalidJsonRequest() { mockChargePointId := "1234" suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) suite.mockServer.On("Write", mockChargePointId, mock.Anything).Return(nil) @@ -105,6 +105,65 @@ func (suite *OcppJTestSuite) TestServerSendInvalidJsonRequest() { assert.IsType(suite.T(), &json.UnsupportedTypeError{}, err) } +func (suite *OcppJTestSuite) TestCentralSystemInvalidMessageHook() { + t := suite.T() + mockChargePointId := "1234" + mockChargePoint := NewMockWebSocket(mockChargePointId) + // Prepare invalid payload + mockID := "1234" + mockPayload := map[string]interface{}{ + "mockValue": float64(1234), + } + serializedPayload, err := json.Marshal(mockPayload) + require.NoError(t, err) + invalidMessage := fmt.Sprintf("[2,\"%v\",\"%s\",%v]", mockID, MockFeatureName, string(serializedPayload)) + expectedError := fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockID, ocppj.FormationViolation, "json: cannot unmarshal number into Go struct field MockRequest.mockValue of type string") + writeHook := suite.mockServer.On("Write", mockChargePointId, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data := args.Get(1).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + // Setup hook 1 + suite.centralSystem.SetInvalidMessageHook(func(client ws.Channel, err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + assert.Equal(t, mockChargePoint.ID(), client.ID()) + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return nil + }) + suite.centralSystem.Start(8887, "/{ws}") + // Trigger incoming invalid CALL + err = suite.mockServer.MessageHandler(mockChargePoint, []byte(invalidMessage)) + ocppErr, ok := err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, ocppj.FormationViolation, ocppErr.Code) + // Setup hook 2 + mockError := ocpp.NewError(ocppj.InternalError, "custom error", mockID) + expectedError = fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockError.MessageId, mockError.Code, mockError.Description) + writeHook.Run(func(args mock.Arguments) { + data := args.Get(1).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.centralSystem.SetInvalidMessageHook(func(client ws.Channel, err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + assert.Equal(t, mockChargePoint.ID(), client.ID()) + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return mockError + }) + // Trigger incoming invalid CALL that returns custom error + err = suite.mockServer.MessageHandler(mockChargePoint, []byte(invalidMessage)) + ocppErr, ok = err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, mockError.Code, ocppErr.Code) + assert.Equal(t, mockError.Description, ocppErr.Description) + assert.Equal(t, mockError.MessageId, ocppErr.MessageId) +} + func (suite *OcppJTestSuite) TestServerSendInvalidCall() { mockChargePointId := "1234" suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) @@ -203,7 +262,8 @@ func (suite *OcppJTestSuite) TestCentralSystemSendConfirmationFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.centralSystem.SendResponse(mockChargePointId, mockUniqueId, mockConfirmation) assert.NotNil(t, err) - assert.Equal(t, "networkError", err.Error()) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) } // SendError @@ -244,9 +304,85 @@ func (suite *OcppJTestSuite) TestCentralSystemSendErrorFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.centralSystem.SendResponse(mockChargePointId, mockUniqueId, mockConfirmation) assert.NotNil(t, err) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) +} + +func (suite *OcppJTestSuite) TestCentralSystemHandleFailedResponse() { + t := suite.T() + msgC := make(chan []byte, 1) + mockChargePointID := "0101" + mockUniqueID := "1234" + suite.mockServer.On("Start", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil) + suite.mockServer.On("Write", mock.AnythingOfType("string"), mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data, ok := args.Get(1).([]byte) + require.True(t, ok) + msgC <- data + }) + suite.centralSystem.Start(8887, "/{ws}") + suite.serverDispatcher.CreateClient(mockChargePointID) + var callResult *ocppj.CallResult + var callError *ocppj.CallError + var err error + // 1. occurrence validation error + mockField := "CallResult.Payload.MockValue" + mockResponse := newMockConfirmation("") + callResult, err = suite.centralSystem.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, mockResponse.GetFeatureName()) + rawResponse := <-msgC + expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, mockResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 2. property constraint validation error + val := "len4" + minParamLength := "5" + mockResponse = newMockConfirmation(val) + callResult, err = suite.centralSystem.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, mockResponse.GetFeatureName()) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Field %s must be minimum %s, but was %d for feature %s",{}]`, + mockUniqueID, ocppj.PropertyConstraintViolation, mockField, minParamLength, len(val), mockResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 3. profile not supported + mockUnsupportedResponse := &MockUnsupportedResponse{MockValue: "someValue"} + callResult, err = suite.centralSystem.CreateCallResult(mockUnsupportedResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, mockUnsupportedResponse.GetFeatureName()) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","couldn't create Call Result for unsupported action %s",{}]`, + mockUniqueID, ocppj.NotSupported, mockUnsupportedResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 4. ocpp error validation failed + invalidErrorCode := "InvalidErrorCode" + callError, err = suite.centralSystem.CreateCallError(mockUniqueID, ocpp.ErrorCode(invalidErrorCode), "", nil) + require.Error(t, err) + require.Nil(t, callError) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Key: 'CallError.ErrorCode' Error:Field validation for 'ErrorCode' failed on the 'errorCode' tag",{}]`, + mockUniqueID, ocppj.GenericError) + assert.Equal(t, expectedErr, string(rawResponse)) + // 5. marshaling err + err = suite.centralSystem.SendError(mockChargePointID, mockUniqueID, ocppj.SecurityError, "", make(chan struct{})) + require.Error(t, err) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","json: unsupported type: chan struct {}",{}]`, mockUniqueID, ocppj.GenericError) + assert.Equal(t, expectedErr, string(rawResponse)) + // 6. network error + rawErr := fmt.Sprintf("couldn't write to websocket. No socket with id %s is open", mockChargePointID) + err = ocpp.NewError(ocppj.GenericError, rawErr, mockUniqueID) + suite.centralSystem.HandleFailedResponseError(mockChargePointID, mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, rawErr) + assert.Equal(t, expectedErr, string(rawResponse)) } -// Handlers +// ----------------- Handlers tests ----------------- func (suite *OcppJTestSuite) TestCentralSystemNewClientHandler() { t := suite.T() diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 3595824a..e0da3a16 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -55,12 +55,14 @@ func (suite *OcppJTestSuite) TestClientStoppedError() { // Simulate websocket internal working suite.mockClient.DisconnectedHandler(nil) }) + call := suite.mockClient.On("IsConnected").Return(true) err := suite.chargePoint.Start("someUrl") require.NoError(t, err) // Stop client suite.chargePoint.Stop() // Send message. Expected error time.Sleep(20 * time.Millisecond) + call.Return(false) assert.False(t, suite.clientDispatcher.IsRunning()) req := newMockRequest("somevalue") err = suite.chargePoint.SendRequest(req) @@ -84,7 +86,7 @@ func (suite *OcppJTestSuite) TestChargePointSendInvalidRequest() { _ = suite.chargePoint.Start("someUrl") mockRequest := newMockRequest("") err := suite.chargePoint.SendRequest(mockRequest) - assert.NotNil(suite.T(), err) + require.NotNil(suite.T(), err) } func (suite *OcppJTestSuite) TestChargePointSendRequestNoValidation() { @@ -110,6 +112,61 @@ func (suite *OcppJTestSuite) TestChargePointSendInvalidJsonRequest() { assert.IsType(suite.T(), &json.UnsupportedTypeError{}, err) } +func (suite *OcppJTestSuite) TestChargePointInvalidMessageHook() { + t := suite.T() + // Prepare invalid payload + mockID := "1234" + mockPayload := map[string]interface{}{ + "mockValue": float64(1234), + } + serializedPayload, err := json.Marshal(mockPayload) + require.NoError(t, err) + invalidMessage := fmt.Sprintf("[2,\"%v\",\"%s\",%v]", mockID, MockFeatureName, string(serializedPayload)) + expectedError := fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockID, ocppj.FormationViolation, "json: cannot unmarshal number into Go struct field MockRequest.mockValue of type string") + writeHook := suite.mockClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data := args.Get(0).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) + // Setup hook 1 + suite.chargePoint.SetInvalidMessageHook(func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return nil + }) + _ = suite.chargePoint.Start("someUrl") + // Trigger incoming invalid CALL + err = suite.mockClient.MessageHandler([]byte(invalidMessage)) + ocppErr, ok := err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, ocppj.FormationViolation, ocppErr.Code) + // Setup hook 2 + mockError := ocpp.NewError(ocppj.InternalError, "custom error", mockID) + expectedError = fmt.Sprintf("[4,\"%v\",\"%v\",\"%v\",{}]", mockError.MessageId, mockError.Code, mockError.Description) + writeHook.Run(func(args mock.Arguments) { + data := args.Get(0).([]byte) + assert.Equal(t, expectedError, string(data)) + }) + suite.chargePoint.SetInvalidMessageHook(func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error { + // Verify the correct fields are passed to the hook. Content is very low-level, since parsing failed + assert.Equal(t, float64(ocppj.CALL), parsedFields[0]) + assert.Equal(t, mockID, parsedFields[1]) + assert.Equal(t, MockFeatureName, parsedFields[2]) + assert.Equal(t, mockPayload, parsedFields[3]) + return mockError + }) + // Trigger incoming invalid CALL that returns custom error + err = suite.mockClient.MessageHandler([]byte(invalidMessage)) + ocppErr, ok = err.(*ocpp.Error) + require.True(t, ok) + assert.Equal(t, mockError.Code, ocppErr.Code) + assert.Equal(t, mockError.Description, ocppErr.Description) + assert.Equal(t, mockError.MessageId, ocppErr.MessageId) +} + func (suite *OcppJTestSuite) TestChargePointSendInvalidCall() { suite.mockClient.On("Write", mock.Anything).Return(nil) suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) @@ -193,7 +250,8 @@ func (suite *OcppJTestSuite) TestChargePointSendConfirmationFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.chargePoint.SendResponse(mockUniqueId, mockConfirmation) assert.NotNil(t, err) - assert.Equal(t, "networkError", err.Error()) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) } // ----------------- SendError tests ----------------- @@ -223,7 +281,79 @@ func (suite *OcppJTestSuite) TestChargePointSendErrorFailed() { mockConfirmation := newMockConfirmation("mockValue") err := suite.chargePoint.SendResponse(mockUniqueId, mockConfirmation) assert.NotNil(t, err) - assert.Equal(t, "networkError", err.Error()) + expectedErr := fmt.Sprintf("ocpp message (%v): GenericError - networkError", mockUniqueId) + assert.ErrorContains(t, err, expectedErr) +} + +func (suite *OcppJTestSuite) TestChargePointHandleFailedResponse() { + t := suite.T() + msgC := make(chan []byte, 1) + mockUniqueID := "1234" + suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) + suite.mockClient.On("Write", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + data, ok := args.Get(0).([]byte) + require.True(t, ok) + msgC <- data + }) + var callResult *ocppj.CallResult + var callError *ocppj.CallError + var err error + // 1. occurrence validation error + mockField := "CallResult.Payload.MockValue" + mockResponse := newMockConfirmation("") + callResult, err = suite.chargePoint.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, mockResponse.GetFeatureName()) + rawResponse := <-msgC + expectedErr := fmt.Sprintf(`[4,"%v","%v","Field %s required but not found for feature %s",{}]`, mockUniqueID, ocppj.OccurrenceConstraintViolation, mockField, mockResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 2. property constraint validation error + val := "len4" + minParamLength := "5" + mockResponse = newMockConfirmation(val) + callResult, err = suite.chargePoint.CreateCallResult(mockResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, mockResponse.GetFeatureName()) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Field %s must be minimum %s, but was %d for feature %s",{}]`, + mockUniqueID, ocppj.PropertyConstraintViolation, mockField, minParamLength, len(val), mockResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 3. profile not supported + mockUnsupportedResponse := &MockUnsupportedResponse{MockValue: "someValue"} + callResult, err = suite.chargePoint.CreateCallResult(mockUnsupportedResponse, mockUniqueID) + require.Error(t, err) + require.Nil(t, callResult) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, mockUnsupportedResponse.GetFeatureName()) + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","couldn't create Call Result for unsupported action %s",{}]`, + mockUniqueID, ocppj.NotSupported, mockUnsupportedResponse.GetFeatureName()) + assert.Equal(t, expectedErr, string(rawResponse)) + // 4. ocpp error validation failed + invalidErrorCode := "InvalidErrorCode" + callError, err = suite.chargePoint.CreateCallError(mockUniqueID, ocpp.ErrorCode(invalidErrorCode), "", nil) + require.Error(t, err) + require.Nil(t, callError) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","Key: 'CallError.ErrorCode' Error:Field validation for 'ErrorCode' failed on the 'errorCode' tag",{}]`, + mockUniqueID, ocppj.GenericError) + assert.Equal(t, expectedErr, string(rawResponse)) + // 5. marshaling err + err = suite.chargePoint.SendError(mockUniqueID, ocppj.SecurityError, "", make(chan struct{})) + require.Error(t, err) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","json: unsupported type: chan struct {}",{}]`, mockUniqueID, ocppj.GenericError) + assert.Equal(t, expectedErr, string(rawResponse)) + // 6. network error + rawErr := "client is currently not connected, cannot send data" + err = ocpp.NewError(ocppj.GenericError, rawErr, mockUniqueID) + suite.chargePoint.HandleFailedResponseError(mockUniqueID, err, "") + rawResponse = <-msgC + expectedErr = fmt.Sprintf(`[4,"%v","%v","%s",{}]`, mockUniqueID, ocppj.GenericError, rawErr) + assert.Equal(t, expectedErr, string(rawResponse)) } // ----------------- Call Handlers tests ----------------- @@ -562,9 +692,11 @@ func (suite *OcppJTestSuite) TestClientReconnected() { require.NotNil(t, call) writeC <- call }).Return(nil) + isConnectedCall := suite.mockClient.On("IsConnected").Return(true) // Start normally err := suite.chargePoint.Start("someUrl") require.Nil(t, err) + assert.True(t, suite.chargePoint.IsConnected()) // Start mocked response routine go func() { counter := 0 @@ -593,16 +725,20 @@ func (suite *OcppJTestSuite) TestClientReconnected() { } // Wait for trigger disconnect after a few responses were returned <-triggerC + isConnectedCall.Return(false) suite.mockClient.DisconnectedHandler(disconnectError) // One message was sent, but all others are still in queue time.Sleep(200 * time.Millisecond) assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) // Wait for some more time and then reconnect time.Sleep(500 * time.Millisecond) + isConnectedCall.Return(true) suite.mockClient.ReconnectedHandler() assert.False(t, suite.clientDispatcher.IsPaused()) assert.True(t, suite.clientDispatcher.IsRunning()) assert.False(t, suite.clientRequestQueue.IsEmpty()) + assert.True(t, suite.chargePoint.IsConnected()) // Wait until remaining messages are sent <-triggerC assert.False(t, suite.clientDispatcher.IsPaused()) @@ -610,6 +746,7 @@ func (suite *OcppJTestSuite) TestClientReconnected() { assert.Equal(t, messagesToQueue, sentMessages) assert.True(t, suite.clientRequestQueue.IsEmpty()) assert.False(t, state.HasPendingRequest()) + assert.True(t, suite.chargePoint.IsConnected()) } // TestClientResponseTimeout ensures that upon a response timeout, the client dispatcher: @@ -655,3 +792,35 @@ func (suite *OcppJTestSuite) TestClientResponseTimeout() { assert.True(t, suite.clientDispatcher.IsRunning()) assert.False(t, state.HasPendingRequest()) } + +func (suite *OcppJTestSuite) TestStopDisconnectedClient() { + t := suite.T() + suite.mockClient.On("Start", mock.AnythingOfType("string")).Return(nil) + suite.mockClient.On("Write", mock.Anything).Return(nil) + suite.mockClient.On("Stop").Return(nil) + call := suite.mockClient.On("IsConnected").Return(true) + // Start normally + err := suite.chargePoint.Start("someUrl") + require.NoError(t, err) + // Trigger network disconnect + disconnectError := fmt.Errorf("some error") + suite.chargePoint.SetOnDisconnectedHandler(func(err error) { + require.Errorf(t, err, disconnectError.Error()) + }) + call.Return(false) + suite.mockClient.DisconnectedHandler(disconnectError) + time.Sleep(100 * time.Millisecond) + // Dispatcher should be paused + assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) + // Stop client while reconnecting + suite.chargePoint.Stop() + time.Sleep(50 * time.Millisecond) + assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) + // Attempt stopping client again + suite.chargePoint.Stop() + time.Sleep(50 * time.Millisecond) + assert.True(t, suite.clientDispatcher.IsPaused()) + assert.False(t, suite.chargePoint.IsConnected()) +} diff --git a/ocppj/client.go b/ocppj/client.go index cdbf16b9..c3f08da3 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -3,6 +3,8 @@ package ocppj import ( "fmt" + "gopkg.in/go-playground/validator.v9" + "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ws" ) @@ -18,6 +20,7 @@ type Client struct { errorHandler func(err *ocpp.Error, details interface{}) onDisconnectedHandler func(err error) onReconnectedHandler func() + invalidMessageHook func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error dispatcher ClientDispatcher RequestState ClientState } @@ -27,6 +30,7 @@ type Client struct { // a state handler and a list of supported profiles (optional). // // You may create a simple new server by using these default values: +// // s := ocppj.NewClient(ws.NewClient(), nil, nil) // // The wsClient parameter cannot be nil. Refer to the ws package for information on how to create and @@ -70,6 +74,24 @@ func (c *Client) SetOnRequestCanceled(handler func(requestId string, request ocp c.dispatcher.SetOnRequestCanceled(handler) } +// SetInvalidMessageHook registers an optional hook for incoming messages that couldn't be parsed. +// This hook is called when a message is received but cannot be parsed to the target OCPP message struct. +// +// The application is notified synchronously of the error. +// The callback provides the raw JSON string, along with the parsed fields. +// The application MUST return as soon as possible, since the hook is called synchronously and awaits a return value. +// +// While the hook does not allow responding to the message directly, +// the return value will be used to send an OCPP error to the other endpoint. +// +// If no handler is registered (or no error is returned by the hook), +// the internal error message is sent to the client without further processing. +// +// Note: Failing to return from the hook will cause the client to block indefinitely. +func (c *Client) SetInvalidMessageHook(hook func(err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error) { + c.invalidMessageHook = hook +} + func (c *Client) SetOnDisconnectedHandler(handler func(err error)) { c.onDisconnectedHandler = handler } @@ -104,8 +126,25 @@ func (c *Client) Start(serverURL string) error { // Stops the client. // The underlying I/O loop is stopped and all pending requests are cleared. func (c *Client) Stop() { + // Overwrite handler to intercept disconnected signal + cleanupC := make(chan struct{}, 1) + if c.IsConnected() { + c.client.SetDisconnectedHandler(func(err error) { + cleanupC <- struct{}{} + }) + } else { + close(cleanupC) + } c.client.Stop() - c.dispatcher.Stop() + if c.dispatcher.IsRunning() { + c.dispatcher.Stop() + } + // Wait for websocket to be cleaned up + <-cleanupC +} + +func (c *Client) IsConnected() bool { + return c.client.IsConnected() } // Sends an OCPP Request to the server. @@ -159,13 +198,14 @@ func (c *Client) SendResponse(requestId string, response ocpp.Response) error { } jsonMessage, err := callResult.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = c.client.Write(jsonMessage); err != nil { - log.Errorf("error sending response [%s]: %v", callResult.UniqueId, err) - return err + log.Errorf("error sending response [%s]: %v", callResult.GetUniqueId(), err) + return ocpp.NewError(GenericError, err.Error(), requestId) } - log.Debugf("sent CALL RESULT [%s]", callResult.UniqueId) + log.Debugf("sent CALL RESULT [%s]", callResult.GetUniqueId()) + log.Debugf("sent JSON message to server: %s", string(jsonMessage)) return nil } @@ -184,13 +224,14 @@ func (c *Client) SendError(requestId string, errorCode ocpp.ErrorCode, descripti } jsonMessage, err := callError.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = c.client.Write(jsonMessage); err != nil { log.Errorf("error sending response error [%s]: %v", callError.UniqueId, err) - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } log.Debugf("sent CALL ERROR [%s]", callError.UniqueId) + log.Debugf("sent JSON message to server: %s", string(jsonMessage)) return nil } @@ -200,9 +241,22 @@ func (c *Client) ocppMessageHandler(data []byte) error { log.Error(err) return err } + log.Debugf("received JSON message from server: %s", string(data)) message, err := c.ParseMessage(parsedJson, c.RequestState) if err != nil { ocppErr := err.(*ocpp.Error) + messageID := ocppErr.MessageId + // Support ad-hoc callback for invalid message handling + if c.invalidMessageHook != nil { + err2 := c.invalidMessageHook(ocppErr, string(data), parsedJson) + // If the hook returns an error, use it as output error. If not, use the original error. + if err2 != nil { + ocppErr = err2 + ocppErr.MessageId = messageID + } + } + err = ocppErr + // Send error to other endpoint if a message ID is available if ocppErr.MessageId != "" { err2 := c.SendError(ocppErr.MessageId, ocppErr.Code, ocppErr.Description, nil) if err2 != nil { @@ -237,6 +291,33 @@ func (c *Client) ocppMessageHandler(data []byte) error { return nil } +// HandleFailedResponseError allows to handle failures while sending responses (either CALL_RESULT or CALL_ERROR). +// It internally analyzes and creates an ocpp.Error based on the given error. +// It will the attempt to send it to the server. +// +// The function helps to prevent starvation on the other endpoint, which is caused by a response never reaching it. +// The method will, however, only attempt to send a default error once. +// If this operation fails, the other endpoint may still starve. +func (c *Client) HandleFailedResponseError(requestID string, err error, featureName string) { + log.Debugf("handling error for failed response [%s]", requestID) + var responseErr *ocpp.Error + // There's several possible errors: invalid profile, invalid payload or send error + switch err.(type) { + case validator.ValidationErrors: + // Validation error + validationErr := err.(validator.ValidationErrors) + responseErr = errorFromValidation(validationErr, requestID, featureName) + case *ocpp.Error: + // Internal OCPP error + responseErr = err.(*ocpp.Error) + case error: + // Unknown error + responseErr = ocpp.NewError(GenericError, err.Error(), requestID) + } + // Send an OCPP error to the target, since no regular response could be sent + _ = c.SendError(requestID, responseErr.Code, responseErr.Description, nil) +} + func (c *Client) onDisconnected(err error) { log.Error("disconnected from server", err) c.dispatcher.Pause() diff --git a/ocppj/dispatcher.go b/ocppj/dispatcher.go index 16891156..f7ef9ab4 100644 --- a/ocppj/dispatcher.go +++ b/ocppj/dispatcher.go @@ -232,6 +232,8 @@ func (d *DefaultClientDispatcher) dispatchNextRequest() { ocpp.NewError(InternalError, err.Error(), bundle.Call.UniqueId)) } } + log.Infof("dispatched request %s to server", bundle.Call.UniqueId) + log.Debugf("sent JSON message to server: %s", string(jsonMessage)) } func (d *DefaultClientDispatcher) Pause() { @@ -569,6 +571,7 @@ func (d *DefaultServerDispatcher) dispatchNextRequest(clientID string) (clientCt clientCtx = clientTimeoutContext{ctx: ctx, cancel: cancel} } log.Infof("dispatched request %s for %s", callID, clientID) + log.Debugf("sent JSON message to %s: %s", clientID, string(jsonMessage)) return } diff --git a/ocppj/dispatcher_test.go b/ocppj/dispatcher_test.go index 5cceac55..a33d5162 100644 --- a/ocppj/dispatcher_test.go +++ b/ocppj/dispatcher_test.go @@ -26,7 +26,7 @@ type ServerDispatcherTestSuite struct { func (s *ServerDispatcherTestSuite) SetupTest() { s.endpoint = ocppj.Server{} - mockProfile := ocpp.NewProfile("mock", MockFeature{}) + mockProfile := ocpp.NewProfile("mock", &MockFeature{}) s.endpoint.AddProfile(mockProfile) s.queueMap = ocppj.NewFIFOQueueMap(10) s.dispatcher = ocppj.NewDefaultServerDispatcher(s.queueMap) @@ -242,7 +242,7 @@ type ClientDispatcherTestSuite struct { func (c *ClientDispatcherTestSuite) SetupTest() { c.endpoint = ocppj.Client{Id: "client1"} - mockProfile := ocpp.NewProfile("mock", MockFeature{}) + mockProfile := ocpp.NewProfile("mock", &MockFeature{}) c.endpoint.AddProfile(mockProfile) c.queue = ocppj.NewFIFOClientQueue(10) c.dispatcher = ocppj.NewDefaultClientDispatcher(c.queue) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index ba531b96..12b3b96d 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -2,6 +2,7 @@ package ocppj import ( + "bytes" "encoding/json" "fmt" "math/rand" @@ -23,6 +24,8 @@ var validationEnabled bool // The internal verbose logger var log logging.Logger +var EscapeHTML = true + func init() { _ = Validate.RegisterValidation("errorCode", IsErrorCodeValid) log = &logging.VoidLogger{} @@ -40,6 +43,12 @@ func SetLogger(logger logging.Logger) { log = logger } +// Allows an instance of ocppj to configure if the message is Marshaled by escaping special caracters like "<", ">", "&" etc +// For more info https://pkg.go.dev/encoding/json#HTMLEscape +func SetHTMLEscape(flag bool) { + EscapeHTML = flag +} + // Allows to enable/disable automatic validation for OCPP messages // (this includes the field constraints defined for every request/response). // The feature may be useful when working with OCPP implementations that don't fully comply to the specs. @@ -111,7 +120,7 @@ func (call *Call) MarshalJSON() ([]byte, error) { fields[1] = call.UniqueId fields[2] = call.Action fields[3] = call.Payload - return json.Marshal(fields) + return jsonMarshal(fields) } // -------------------- Call Result -------------------- @@ -137,7 +146,7 @@ func (callResult *CallResult) MarshalJSON() ([]byte, error) { fields[0] = int(callResult.MessageTypeId) fields[1] = callResult.UniqueId fields[2] = callResult.Payload - return json.Marshal(fields) + return jsonMarshal(fields) } // -------------------- Call Error -------------------- @@ -148,7 +157,7 @@ type CallError struct { MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=4"` UniqueId string `json:"uniqueId" validate:"required,max=36"` ErrorCode ocpp.ErrorCode `json:"errorCode" validate:"errorCode"` - ErrorDescription string `json:"errorDescription" validate:"required"` + ErrorDescription string `json:"errorDescription" validate:"omitempty"` ErrorDetails interface{} `json:"errorDetails" validate:"omitempty"` } @@ -166,7 +175,11 @@ func (callError *CallError) MarshalJSON() ([]byte, error) { fields[1] = callError.UniqueId fields[2] = callError.ErrorCode fields[3] = callError.ErrorDescription - fields[4] = callError.ErrorDetails + if callError.ErrorDetails == nil { + fields[4] = struct{}{} + } else { + fields[4] = callError.ErrorDetails + } return ocppMessageToJson(fields) } @@ -177,11 +190,16 @@ const ( MessageTypeNotSupported ocpp.ErrorCode = "MessageTypeNotSupported" // A message with an Message Type Number received that is not supported by this implementation. ProtocolError ocpp.ErrorCode = "ProtocolError" // Payload for Action is incomplete. SecurityError ocpp.ErrorCode = "SecurityError" // During the processing of Action a security issue occurred preventing receiver from completing the Action successfully. - FormationViolation ocpp.ErrorCode = "FormationViolation" // Payload for Action is syntactically incorrect or not conform the PDU structure for Action. PropertyConstraintViolation ocpp.ErrorCode = "PropertyConstraintViolation" // Payload is syntactically correct but at least one field contains an invalid value. OccurrenceConstraintViolation ocpp.ErrorCode = "OccurrenceConstraintViolation" // Payload for Action is syntactically correct but at least one of the fields violates occurrence constraints. TypeConstraintViolation ocpp.ErrorCode = "TypeConstraintViolation" // Payload for Action is syntactically correct but at least one of the fields violates data type constraints (e.g. “somestring”: 12). GenericError ocpp.ErrorCode = "GenericError" // Any other error not covered by the previous ones. + FormatViolationV2 ocpp.ErrorCode = "FormatViolation" // Payload for Action is syntactically incorrect. This is only valid for OCPP 2.0.1 + FormatViolationV16 ocpp.ErrorCode = "FormationViolation" // Payload for Action is syntactically incorrect or not conform the PDU structure for Action. This is only valid for OCPP 1.6 +) + +var ( + FormationViolation = FormatViolationV16 // Used as constant, but can be overwritten depending on protocol version. Sett FormatViolationV16 and FormatViolationV2. ) func IsErrorCodeValid(fl validator.FieldLevel) bool { @@ -214,7 +232,7 @@ func ParseJsonMessage(dataJson string) ([]interface{}, error) { } func ocppMessageToJson(message interface{}) ([]byte, error) { - jsonData, err := json.Marshal(message) + jsonData, err := jsonMarshal(message) if err != nil { return nil, err } @@ -276,6 +294,15 @@ func errorFromValidation(validationErrors validator.ValidationErrors, messageId return ocpp.NewError(GenericError, fmt.Sprintf("%v", validationErrors.Error()), messageId) } +// Marshals data by manipulating EscapeHTML property of encoder +func jsonMarshal(t interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(EscapeHTML) + err := encoder.Encode(t) + return bytes.TrimRight(buffer.Bytes(), "\n"), err +} + // -------------------- Endpoint -------------------- // An OCPP-J endpoint is one of the two entities taking part in the communication. @@ -367,7 +394,11 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl if len(arr) != 4 { return nil, ocpp.NewError(FormationViolation, "Invalid Call message. Expected array length 4", uniqueId) } - action := arr[2].(string) + action, ok := arr[2].(string) + if !ok { + return nil, ocpp.NewError(FormationViolation, fmt.Sprintf("Invalid element %v at 2, expected action (string)", arr[2]), "") + } + profile, ok := endpoint.GetProfileForFeature(action) if !ok { return nil, ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", action), uniqueId) @@ -421,7 +452,10 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl if len(arr) > 4 { details = arr[4] } - rawErrorCode := arr[2].(string) + rawErrorCode, ok := arr[2].(string) + if !ok { + return nil, ocpp.NewError(FormationViolation, fmt.Sprintf("Invalid element %v at 2, expected rawErrorCode (string)", arr[2]), rawErrorCode) + } errorCode := ocpp.ErrorCode(rawErrorCode) errorDescription := "" if v, ok := arr[3].(string); ok { @@ -478,7 +512,7 @@ func (endpoint *Endpoint) CreateCallResult(confirmation ocpp.Response, uniqueId action := confirmation.GetFeatureName() profile, _ := endpoint.GetProfileForFeature(action) if profile == nil { - return nil, fmt.Errorf("Couldn't create Call Result for unsupported action %v", action) + return nil, ocpp.NewError(NotSupported, fmt.Sprintf("couldn't create Call Result for unsupported action %v", action), uniqueId) } callResult := CallResult{ MessageTypeId: CALL_RESULT, diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index 9b0253a6..c9d73385 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -4,9 +4,12 @@ import ( "crypto/tls" "fmt" "net" + "net/http" "reflect" "testing" + ut "github.com/go-playground/universal-translator" + "github.com/lorenzodonini/ocpp-go/logging" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -51,6 +54,7 @@ type MockWebsocketServer struct { ws.WsServer MessageHandler func(ws ws.Channel, data []byte) error NewClientHandler func(ws ws.Channel) + CheckClientHandler ws.CheckClientHandler DisconnectedClientHandler func(ws ws.Channel) errC chan error } @@ -100,6 +104,10 @@ func (websocketServer *MockWebsocketServer) NewClient(websocketId string, client websocketServer.MethodCalled("NewClient", websocketId, client) } +func (websocketServer *MockWebsocketServer) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { + websocketServer.CheckClientHandler = handler +} + // ---------------------- MOCK WEBSOCKET CLIENT ---------------------- type MockWebsocketClient struct { @@ -162,6 +170,11 @@ func (websocketClient *MockWebsocketClient) Errors() <-chan error { return websocketClient.errC } +func (websocketClient *MockWebsocketClient) IsConnected() bool { + args := websocketClient.MethodCalled("IsConnected") + return args.Bool(0) +} + // ---------------------- MOCK FEATURE ---------------------- const ( MockFeatureName = "Mock" @@ -182,23 +195,23 @@ type MockFeature struct { mock.Mock } -func (f MockFeature) GetFeatureName() string { +func (f *MockFeature) GetFeatureName() string { return MockFeatureName } -func (f MockFeature) GetRequestType() reflect.Type { +func (f *MockFeature) GetRequestType() reflect.Type { return reflect.TypeOf(MockRequest{}) } -func (f MockFeature) GetResponseType() reflect.Type { +func (f *MockFeature) GetResponseType() reflect.Type { return reflect.TypeOf(MockConfirmation{}) } -func (r MockRequest) GetFeatureName() string { +func (r *MockRequest) GetFeatureName() string { return MockFeatureName } -func (c MockConfirmation) GetFeatureName() string { +func (c *MockConfirmation) GetFeatureName() string { return MockFeatureName } @@ -210,6 +223,14 @@ func newMockConfirmation(value string) *MockConfirmation { return &MockConfirmation{MockValue: value} } +type MockUnsupportedResponse struct { + MockValue string `json:"mockValue" validate:"required,min=5"` +} + +func (m *MockUnsupportedResponse) GetFeatureName() string { + return "SomeRandomFeature" +} + // ---------------------- COMMON UTILITY METHODS ---------------------- func NewWebsocketServer(t *testing.T, onMessage func(data []byte) ([]byte, error)) *ws.Server { @@ -343,7 +364,7 @@ type OcppJTestSuite struct { } func (suite *OcppJTestSuite) SetupTest() { - mockProfile := ocpp.NewProfile("mock", MockFeature{}) + mockProfile := ocpp.NewProfile("mock", &MockFeature{}) mockClient := MockWebsocketClient{} mockServer := MockWebsocketServer{} suite.mockClient = &mockClient @@ -608,6 +629,26 @@ func (suite *OcppJTestSuite) TestParseMessageInvalidCall() { assert.Equal(t, "Invalid Call message. Expected array length 4", protoErr.Description) } +func (suite *OcppJTestSuite) TestParseMessageInvalidActionCall() { + t := suite.T() + mockMessage := make([]interface{}, 4) + messageId := "12345" + mockRequest := newMockRequest("") + // Test invalid message length + mockMessage[0] = float64(ocppj.CALL) // Message Type ID + mockMessage[1] = messageId // Unique ID + mockMessage[2] = float64(42) // Wrong type on action parameter + mockMessage[3] = mockRequest + message, err := suite.chargePoint.ParseMessage(mockMessage, suite.chargePoint.RequestState) + require.Nil(t, message) + require.Error(t, err) + protoErr := err.(*ocpp.Error) + require.NotNil(t, protoErr) + assert.Equal(t, protoErr.MessageId, "") // unique id is never set after invalid type cast return + assert.Equal(t, ocppj.FormationViolation, protoErr.Code) + assert.Equal(t, "Invalid element 42 at 2, expected action (string)", protoErr.Description) +} + func (suite *OcppJTestSuite) TestParseMessageInvalidCallResult() { t := suite.T() mockMessage := make([]interface{}, 3) @@ -643,6 +684,27 @@ func (suite *OcppJTestSuite) TestParseMessageInvalidCallError() { assert.Equal(t, "Invalid Call Error message. Expected array length >= 4", protoErr.Description) } +func (suite *OcppJTestSuite) TestParseMessageInvalidRawErrorCode() { + t := suite.T() + mockMessage := make([]interface{}, 5) + messageId := "12345" + pendingRequest := newMockRequest("request") + mockMessage[0] = float64(ocppj.CALL_ERROR) // Message Type ID + mockMessage[1] = messageId // Unique ID + mockMessage[2] = float64(42) // test invalid typecast + mockMessage[3] = "error description" + mockMessage[4] = "error details" + suite.chargePoint.RequestState.AddPendingRequest(messageId, pendingRequest) // Manually add a pending request, so that response is not rejected + message, err := suite.chargePoint.ParseMessage(mockMessage, suite.chargePoint.RequestState) + require.Nil(t, message) + require.Error(t, err) + protoErr := err.(*ocpp.Error) + require.NotNil(t, protoErr) + assert.Equal(t, protoErr.MessageId, "") // unique id is never set after invalid type cast return + assert.Equal(t, ocppj.FormationViolation, protoErr.Code) + assert.Equal(t, "Invalid element 42 at 2, expected rawErrorCode (string)", protoErr.Description) +} + func (suite *OcppJTestSuite) TestParseMessageInvalidRequest() { t := suite.T() mockMessage := make([]interface{}, 4) @@ -770,6 +832,27 @@ func (suite *OcppJTestSuite) TestLogger() { }) } +type MockValidationError struct { + tag string + namespace string + param string + value string + typ reflect.Type +} + +func (m MockValidationError) ActualTag() string { return m.tag } +func (m MockValidationError) Tag() string { return m.tag } +func (m MockValidationError) Namespace() string { return m.namespace } +func (m MockValidationError) StructNamespace() string { return m.namespace } +func (m MockValidationError) Field() string { return m.namespace } +func (m MockValidationError) StructField() string { return m.namespace } +func (m MockValidationError) Value() interface{} { return m.value } +func (m MockValidationError) Param() string { return m.param } +func (m MockValidationError) Kind() reflect.Kind { return m.typ.Kind() } +func (m MockValidationError) Type() reflect.Type { return m.typ } +func (m MockValidationError) Translate(ut ut.Translator) string { return "" } +func (m MockValidationError) Error() string { return fmt.Sprintf("some error for value %s", m.value) } + func TestMockOcppJ(t *testing.T) { suite.Run(t, new(ClientQueueTestSuite)) suite.Run(t, new(ServerQueueMapTestSuite)) diff --git a/ocppj/server.go b/ocppj/server.go index f547c447..7d651329 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -4,6 +4,8 @@ import ( "fmt" "sync" + "gopkg.in/go-playground/validator.v9" + "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ws" ) @@ -13,11 +15,13 @@ import ( type Server struct { Endpoint server ws.WsServer + checkClientHandler ws.CheckClientHandler newClientHandler ClientHandler disconnectedClientHandler ClientHandler requestHandler RequestHandler responseHandler ResponseHandler errorHandler ErrorHandler + invalidMessageHook InvalidMessageHook dispatcher ServerDispatcher RequestState ServerState waitGroup sync.WaitGroup @@ -28,6 +32,7 @@ type ClientHandler func(client ws.Channel) type RequestHandler func(client ws.Channel, request ocpp.Request, requestId string, action string) type ResponseHandler func(client ws.Channel, response ocpp.Response, requestId string) type ErrorHandler func(client ws.Channel, err *ocpp.Error, details interface{}) +type InvalidMessageHook func(client ws.Channel, err *ocpp.Error, rawJson string, parsedFields []interface{}) *ocpp.Error // Creates a new Server endpoint. // Requires a a websocket server. Optionally a structure for queueing/dispatching requests, @@ -79,6 +84,24 @@ func (s *Server) SetErrorHandler(handler ErrorHandler) { s.errorHandler = handler } +// SetInvalidMessageHook registers an optional hook for incoming messages that couldn't be parsed. +// This hook is called when a message is received but cannot be parsed to the target OCPP message struct. +// +// The application is notified synchronously of the error. +// The callback provides the raw JSON string, along with the parsed fields. +// The application MUST return as soon as possible, since the hook is called synchronously and awaits a return value. +// +// The hook does not allow responding to the message directly, +// but the return value will be used to send an OCPP error to the other endpoint. +// +// If no handler is registered (or no error is returned by the hook), +// the internal error message is sent to the client without further processing. +// +// Note: Failing to return from the hook will cause the handler for this client to block indefinitely. +func (s *Server) SetInvalidMessageHook(hook InvalidMessageHook) { + s.invalidMessageHook = hook +} + // Registers a handler for canceled request messages. func (s *Server) SetCanceledRequestHandler(handler CanceledRequestHandler) { s.dispatcher.SetOnRequestCanceled(handler) @@ -89,6 +112,11 @@ func (s *Server) SetNewClientHandler(handler ClientHandler) { s.newClientHandler = handler } +// Registers a handler for validate incoming client connections. +func (s *Server) SetNewClientValidationHandler(handler ws.CheckClientHandler) { + s.checkClientHandler = handler +} + // Registers a handler for client disconnections. func (s *Server) SetDisconnectedClientHandler(handler ClientHandler) { s.disconnectedClientHandler = handler @@ -103,6 +131,7 @@ func (s *Server) SetDisconnectedClientHandler(handler ClientHandler) { func (s *Server) Start(listenPort int, listenPath string) { // Set internal message handler s.stopped = make(chan struct{}) + s.server.SetCheckClientHandler(s.checkClientHandler) s.server.SetNewClientHandler(s.onClientConnected) s.server.SetDisconnectedClientHandler(s.onClientDisconnected) s.server.SetMessageHandler(s.ocppMessageHandler) @@ -171,13 +200,13 @@ func (s *Server) SendResponse(clientID string, requestId string, response ocpp.R } jsonMessage, err := callResult.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = s.server.Write(clientID, jsonMessage); err != nil { - log.Errorf("error sending response [%s] to %s: %v", callResult.UniqueId, clientID, err) - return err + log.Errorf("error sending response [%s] to %s: %v", callResult.GetUniqueId(), clientID, err) + return ocpp.NewError(GenericError, err.Error(), requestId) } - log.Debugf("sent CALL RESULT [%s] for %s", callResult.UniqueId, clientID) + log.Debugf("sent CALL RESULT [%s] for %s", callResult.GetUniqueId(), clientID) log.Debugf(">>> %s", string(jsonMessage)) return nil } @@ -197,11 +226,11 @@ func (s *Server) SendError(clientID string, requestId string, errorCode ocpp.Err } jsonMessage, err := callError.MarshalJSON() if err != nil { - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } if err = s.server.Write(clientID, jsonMessage); err != nil { log.Errorf("error sending response error [%s] to %s: %v", callError.UniqueId, clientID, err) - return err + return ocpp.NewError(GenericError, err.Error(), requestId) } log.Debugf("sent CALL ERROR [%s] for %s", callError.UniqueId, clientID) return nil @@ -215,11 +244,24 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { log.Error(err) return err } + // Get pending requests for client pending := s.RequestState.GetClientState(wsChannel.ID()) message, err := s.ParseMessage(parsedJson, pending) if err != nil { ocppErr := err.(*ocpp.Error) + messageID := ocppErr.MessageId + // Support ad-hoc callback for invalid message handling + if s.invalidMessageHook != nil { + err2 := s.invalidMessageHook(wsChannel, ocppErr, string(data), parsedJson) + // If the hook returns an error, use it as output error. If not, use the original error. + if err2 != nil { + ocppErr = err2 + ocppErr.MessageId = messageID + } + } + err = ocppErr + // Send error to other endpoint if a message ID is available if ocppErr.MessageId != "" { err2 := s.SendError(wsChannel.ID(), ocppErr.MessageId, ocppErr.Code, ocppErr.Description, nil) if err2 != nil { @@ -234,7 +276,9 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { case CALL: call := message.(*Call) log.Debugf("handling incoming CALL [%s, %s] from %s", call.UniqueId, call.Action, wsChannel.ID()) - s.requestHandler(wsChannel, call.Payload, call.UniqueId, call.Action) + if s.requestHandler != nil { + s.requestHandler(wsChannel, call.Payload, call.UniqueId, call.Action) + } case CALL_RESULT: callResult := message.(*CallResult) log.Debugf("handling incoming CALL RESULT [%s] from %s", callResult.UniqueId, wsChannel.ID()) @@ -254,6 +298,33 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { return nil } +// HandleFailedResponseError allows to handle failures while sending responses (either CALL_RESULT or CALL_ERROR). +// It internally analyzes and creates an ocpp.Error based on the given error. +// It will the attempt to send it to the client. +// +// The function helps to prevent starvation on the other endpoint, which is caused by a response never reaching it. +// The method will, however, only attempt to send a default error once. +// If this operation fails, the other endpoint may still starve. +func (s *Server) HandleFailedResponseError(clientID string, requestID string, err error, featureName string) { + log.Debugf("handling error for failed response [%s]", requestID) + var responseErr *ocpp.Error + // There's several possible errors: invalid profile, invalid payload or send error + switch err.(type) { + case validator.ValidationErrors: + // Validation error + validationErr := err.(validator.ValidationErrors) + responseErr = errorFromValidation(validationErr, requestID, featureName) + case *ocpp.Error: + // Internal OCPP error + responseErr = err.(*ocpp.Error) + case error: + // Unknown error + responseErr = ocpp.NewError(GenericError, err.Error(), requestID) + } + // Send an OCPP error to the target, since no regular response could be sent + _ = s.SendError(clientID, requestID, responseErr.Code, responseErr.Description, nil) +} + func (s *Server) onClientConnected(ws ws.Channel) { s.waitGroup.Add(1) defer s.waitGroup.Done() diff --git a/ws/websocket.go b/ws/websocket.go index 3dc9f8a7..4b1817e5 100644 --- a/ws/websocket.go +++ b/ws/websocket.go @@ -17,10 +17,10 @@ import ( "sync" "time" - "github.com/lorenzodonini/ocpp-go/logging" - "github.com/gorilla/mux" "github.com/gorilla/websocket" + + "github.com/lorenzodonini/ocpp-go/logging" ) const ( @@ -150,6 +150,8 @@ func (e HttpConnectionError) Error() string { // ---------------------- SERVER ---------------------- +type CheckClientHandler func(id string, r *http.Request) bool + // WsServer defines a websocket server, which passively listens for incoming connections on ws or wss protocol. // The offered API are of asynchronous nature, and each incoming connection/message is handled using callbacks. // @@ -235,6 +237,9 @@ type WsServer interface { // By default, if the Origin header is present in the request, and the Origin host is not equal // to the Host request header, the websocket handshake fails. SetCheckOriginHandler(handler func(r *http.Request) bool) + // SetCheckClientHandler sets a handler for validate incoming websocket connections, allowing to perform + // custom client connection checks. + SetCheckClientHandler(handler func(id string, r *http.Request) bool) // Addr gives the address on which the server is listening, useful if, for // example, the port is system-defined (set to 0). Addr() *net.TCPAddr @@ -247,6 +252,7 @@ type Server struct { connections map[string]*WebSocket httpServer *http.Server messageHandler func(ws Channel, data []byte) error + checkClientHandler func(id string, r *http.Request) bool newClientHandler func(ws Channel) disconnectedHandler func(ws Channel) basicAuthHandler func(username string, password string) bool @@ -257,14 +263,17 @@ type Server struct { errC chan error connMutex sync.RWMutex addr *net.TCPAddr + httpHandler *mux.Router } // Creates a new simple websocket server (the websockets are not secured). func NewServer() *Server { + router := mux.NewRouter() return &Server{ httpServer: &http.Server{}, timeoutConfig: NewServerTimeoutConfig(), upgrader: websocket.Upgrader{Subprotocols: []string{}}, + httpHandler: router, } } @@ -283,6 +292,7 @@ func NewServer() *Server { // If no tlsConfig parameter is passed, the server will by default // not perform any client certificate verification. func NewTLSServer(certificatePath string, certificateKey string, tlsConfig *tls.Config) *Server { + router := mux.NewRouter() return &Server{ tlsCertificatePath: certificatePath, tlsCertificateKey: certificateKey, @@ -291,6 +301,7 @@ func NewTLSServer(certificatePath string, certificateKey string, tlsConfig *tls. }, timeoutConfig: NewServerTimeoutConfig(), upgrader: websocket.Upgrader{Subprotocols: []string{}}, + httpHandler: router, } } @@ -298,6 +309,10 @@ func (server *Server) SetMessageHandler(handler func(ws Channel, data []byte) er server.messageHandler = handler } +func (server *Server) SetCheckClientHandler(handler func(id string, r *http.Request) bool) { + server.checkClientHandler = handler +} + func (server *Server) SetNewClientHandler(handler func(ws Channel)) { server.newClientHandler = handler } @@ -346,11 +361,12 @@ func (server *Server) Addr() *net.TCPAddr { return server.addr } +func (server *Server) AddHttpHandler(listenPath string, handler func(w http.ResponseWriter, r *http.Request)) { + server.httpHandler.HandleFunc(listenPath, handler) +} + func (server *Server) Start(port int, listenPath string) { - router := mux.NewRouter() - router.HandleFunc(listenPath, func(w http.ResponseWriter, r *http.Request) { - server.wsHandler(w, r) - }) + server.connections = make(map[string]*WebSocket) if server.httpServer == nil { server.httpServer = &http.Server{} @@ -358,7 +374,11 @@ func (server *Server) Start(port int, listenPath string) { addr := fmt.Sprintf(":%v", port) server.httpServer.Addr = addr - server.httpServer.Handler = router + + server.AddHttpHandler(listenPath, func(w http.ResponseWriter, r *http.Request) { + server.wsHandler(w, r) + }) + server.httpServer.Handler = server.httpHandler ln, err := net.Listen("tcp", addr) if err != nil { @@ -468,6 +488,16 @@ out: return } } + + if server.checkClientHandler != nil { + ok := server.checkClientHandler(id, r) + if !ok { + server.error(fmt.Errorf("client validation: invalid client")) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + } + // Upgrade websocket conn, err := server.upgrader.Upgrade(w, r, responseHeader) if err != nil { @@ -986,9 +1016,7 @@ func (client *Client) IsConnected() bool { } func (client *Client) Write(data []byte) error { - client.mutex.Lock() - defer client.mutex.Unlock() - if !client.connected { + if !client.IsConnected() { return fmt.Errorf("client is currently not connected, cannot send data") } log.Debugf("queuing data for server") @@ -1061,13 +1089,14 @@ func (client *Client) Stop() { } client.mutex.Unlock() // Notify reconnection goroutine to stop (if any) - close(client.reconnectC) + if client.reconnectC != nil { + close(client.reconnectC) + } if client.errC != nil { close(client.errC) client.errC = nil } // Wait for connection to actually close - } func (client *Client) error(err error) { diff --git a/ws/websocket_test.go b/ws/websocket_test.go index a80371c6..8267d8db 100644 --- a/ws/websocket_test.go +++ b/ws/websocket_test.go @@ -665,6 +665,50 @@ func TestCustomOriginHeaderHandler(t *testing.T) { wsServer.Stop() } +func TestCustomCheckClientHandler(t *testing.T) { + invalidTestPath := "/ws/invalid-testws" + id := path.Base(testPath) + connected := make(chan bool) + wsServer := newWebsocketServer(t, func(data []byte) ([]byte, error) { + assert.Fail(t, "no message should be received from client!") + return nil, nil + }) + wsServer.SetNewClientHandler(func(ws Channel) { + connected <- true + }) + wsServer.SetCheckClientHandler(func(clientId string, r *http.Request) bool { + return id == clientId + }) + go wsServer.Start(serverPort, serverPath) + time.Sleep(500 * time.Millisecond) + + // Test message + wsClient := newWebsocketClient(t, func(data []byte) ([]byte, error) { + assert.Fail(t, "no message should be received from server!") + return nil, nil + }) + + host := fmt.Sprintf("localhost:%v", serverPort) + // Set invalid client (not /ws/testws) + u := url.URL{Scheme: "ws", Host: host, Path: invalidTestPath} + // Attempt to connect and expect invalid client id error + err := wsClient.Start(u.String()) + require.Error(t, err) + httpErr, ok := err.(HttpConnectionError) + require.True(t, ok) + assert.Equal(t, http.StatusUnauthorized, httpErr.HttpCode) + assert.Equal(t, "websocket: bad handshake", httpErr.Message) + + // Re-attempt with correct client id + u = url.URL{Scheme: "ws", Host: host, Path: testPath} + err = wsClient.Start(u.String()) + require.NoError(t, err) + result := <-connected + assert.True(t, result) + // Cleanup + wsServer.Stop() +} + func TestValidClientTLSCertificate(t *testing.T) { // Create self-signed TLS certificate clientCertFilename := "/tmp/client.pem" @@ -777,7 +821,7 @@ func TestInvalidClientTLSCertificate(t *testing.T) { assert.NotNil(t, err) netError, ok := err.(net.Error) require.True(t, ok) - assert.Equal(t, "remote error: tls: bad certificate", netError.Error()) // tls.alertBadCertificate = 42 + assert.Equal(t, "remote error: tls: unknown certificate authority", netError.Error()) // tls.alertUnknownCA = 48 // Cleanup wsServer.Stop() }