From 6ecb834092bb23dd9d672cadc99f4fae198e7982 Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Thu, 19 Jun 2025 01:21:08 +0200 Subject: [PATCH 01/12] feat: initial support for 2.1 (wip) --- ocpp/ocpp.go | 1 + ocppj/ocppj.go | 136 ++++++++++++++++++++++--------------------------- ocppj/parse.go | 69 +++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 76 deletions(-) create mode 100644 ocppj/parse.go diff --git a/ocpp/ocpp.go b/ocpp/ocpp.go index 09019dd6..3f1e4c0d 100644 --- a/ocpp/ocpp.go +++ b/ocpp/ocpp.go @@ -120,4 +120,5 @@ const ( _ Dialect = iota V16 V2 + V21 ) diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index 3591f95e..63a4a9d5 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -2,13 +2,10 @@ package ocppj import ( - "bytes" "encoding/json" "fmt" - "math/rand" - "reflect" - "github.com/lorenzodonini/ocpp-go/logging" + "math/rand" "gopkg.in/go-playground/validator.v9" @@ -65,9 +62,11 @@ func SetMessageValidation(enabled bool) { type MessageType int const ( - CALL MessageType = 2 - CALL_RESULT MessageType = 3 - CALL_ERROR MessageType = 4 + CALL MessageType = 2 + CALL_RESULT MessageType = 3 + CALL_ERROR MessageType = 4 + CALL_RESULT_ERROR MessageType = 5 + SEND MessageType = 6 ) // An OCPP-J message. @@ -183,12 +182,41 @@ func (callError *CallError) MarshalJSON() ([]byte, error) { return ocppMessageToJson(fields) } +// -------------------- Call -------------------- + +// An OCPP-J SEND message, containing an OCPP Request. +type Send struct { + Message `validate:"-"` + MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=2"` + UniqueId string `json:"uniqueId" validate:"required,max=36"` + Action string `json:"action" validate:"required,max=36"` + Payload ocpp.Request `json:"payload" validate:"required"` +} + +func (call *Send) GetMessageTypeId() MessageType { + return call.MessageTypeId +} + +func (call *Send) GetUniqueId() string { + return call.UniqueId +} + +func (call *Send) MarshalJSON() ([]byte, error) { + fields := make([]interface{}, 4) + fields[0] = int(call.MessageTypeId) + fields[1] = call.UniqueId + fields[2] = call.Action + fields[3] = call.Payload + return jsonMarshal(fields) +} + const ( NotImplemented ocpp.ErrorCode = "NotImplemented" // Requested Action is not known by receiver. NotSupported ocpp.ErrorCode = "NotSupported" // Requested Action is recognized but not supported by the receiver. InternalError ocpp.ErrorCode = "InternalError" // An internal error occurred and the receiver was not able to process the requested Action successfully. MessageTypeNotSupported ocpp.ErrorCode = "MessageTypeNotSupported" // A message with a Message Type Number received that is not supported by this implementation. ProtocolError ocpp.ErrorCode = "ProtocolError" // Payload for Action is incomplete. + RpcFrameworkError ocpp.ErrorCode = "RpcFrameworkError" // Content of the call is not a valid RPC Request, for example: MessageId could not be read. SecurityError ocpp.ErrorCode = "SecurityError" // During the processing of Action a security issue occurred preventing receiver from completing the Action successfully. PropertyConstraintViolation ocpp.ErrorCode = "PropertyConstraintViolation" // Payload is syntactically correct but at least one field contains an invalid value. OccurrenceConstraintViolationV2 ocpp.ErrorCode = "OccurrenceConstraintViolation" // Payload for Action is syntactically correct but at least one of the fields violates occurrence constraints. @@ -207,7 +235,7 @@ func FormatErrorType(d dialector) ocpp.ErrorCode { switch d.Dialect() { case ocpp.V16: return FormatViolationV16 - case ocpp.V2: + case ocpp.V2, ocpp.V21: return FormatViolationV2 default: panic(fmt.Sprintf("invalid dialect: %v", d)) @@ -218,7 +246,7 @@ func OccurrenceConstraintErrorType(d dialector) ocpp.ErrorCode { switch d.Dialect() { case ocpp.V16: return OccurrenceConstraintViolationV16 - case ocpp.V2: + case ocpp.V2, ocpp.V21: return OccurrenceConstraintViolationV2 default: panic(fmt.Sprintf("invalid dialect: %v", d)) @@ -228,7 +256,10 @@ func OccurrenceConstraintErrorType(d dialector) ocpp.ErrorCode { func IsErrorCodeValid(fl validator.FieldLevel) bool { code := ocpp.ErrorCode(fl.Field().String()) switch code { - case NotImplemented, NotSupported, InternalError, MessageTypeNotSupported, ProtocolError, SecurityError, FormatViolationV16, FormatViolationV2, PropertyConstraintViolation, OccurrenceConstraintViolationV16, OccurrenceConstraintViolationV2, TypeConstraintViolation, GenericError: + case NotImplemented, NotSupported, InternalError, MessageTypeNotSupported, + ProtocolError, SecurityError, FormatViolationV16, FormatViolationV2, + PropertyConstraintViolation, OccurrenceConstraintViolationV16, OccurrenceConstraintViolationV2, + TypeConstraintViolation, GenericError: return true } return false @@ -236,24 +267,6 @@ func IsErrorCodeValid(fl validator.FieldLevel) bool { // -------------------- Logic -------------------- -// Unmarshals an OCPP-J json object from a byte array. -// Returns the array of elements contained in the message. -func ParseRawJsonMessage(dataJson []byte) ([]interface{}, error) { - var arr []interface{} - err := json.Unmarshal(dataJson, &arr) - if err != nil { - return nil, err - } - return arr, nil -} - -// Unmarshals an OCPP-J json object from a JSON string. -// Returns the array of elements contained in the message. -func ParseJsonMessage(dataJson string) ([]interface{}, error) { - rawJson := []byte(dataJson) - return ParseRawJsonMessage(rawJson) -} - func ocppMessageToJson(message interface{}) ([]byte, error) { jsonData, err := jsonMarshal(message) if err != nil { @@ -317,15 +330,6 @@ func errorFromValidation(d dialector, validationErrors validator.ValidationError 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. @@ -372,40 +376,6 @@ func (endpoint *Endpoint) GetProfileForFeature(featureName string) (*ocpp.Profil return nil, false } -func parseRawJsonRequest(raw interface{}, requestType reflect.Type) (ocpp.Request, error) { - if raw == nil { - raw = &struct{}{} - } - bytes, err := json.Marshal(raw) - if err != nil { - return nil, err - } - request := reflect.New(requestType).Interface() - err = json.Unmarshal(bytes, &request) - if err != nil { - return nil, err - } - result := request.(ocpp.Request) - return result, nil -} - -func parseRawJsonConfirmation(raw interface{}, confirmationType reflect.Type) (ocpp.Response, error) { - if raw == nil { - raw = &struct{}{} - } - bytes, err := json.Marshal(raw) - if err != nil { - return nil, err - } - confirmation := reflect.New(confirmationType).Interface() - err = json.Unmarshal(bytes, &confirmation) - if err != nil { - return nil, err - } - result := confirmation.(ocpp.Response) - return result, nil -} - // Parses an OCPP-J message. The function expects an array of elements, as contained in the JSON message. // // Pending requests are automatically cleared, in case the received message is a CallResponse or CallError. @@ -414,20 +384,25 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl if len(arr) < 3 { return nil, ocpp.NewError(FormatErrorType(endpoint), "Invalid message. Expected array length >= 3", "") } + rawTypeId, ok := arr[0].(float64) if !ok { return nil, ocpp.NewError(FormatErrorType(endpoint), fmt.Sprintf("Invalid element %v at 0, expected message type (int)", arr[0]), "") } + typeId := MessageType(rawTypeId) uniqueId, ok := arr[1].(string) if !ok { return nil, ocpp.NewError(FormatErrorType(endpoint), fmt.Sprintf("Invalid element %v at 1, expected unique ID (string)", arr[1]), uniqueId) } + if uniqueId == "" { return nil, ocpp.NewError(FormatErrorType(endpoint), "Invalid unique ID, cannot be empty", uniqueId) } - // Parse message - if typeId == CALL { + + // Parse message base on type + switch typeId { + case CALL: if len(arr) != 4 { return nil, ocpp.NewError(FormatErrorType(endpoint), "Invalid Call message. Expected array length 4", uniqueId) } @@ -455,7 +430,7 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl return nil, errorFromValidation(endpoint, err.(validator.ValidationErrors), uniqueId, action) } return &call, nil - } else if typeId == CALL_RESULT { + case CALL_RESULT: request, ok := pendingRequestState.GetPendingRequest(uniqueId) if !ok { log.Infof("No previous request %v sent. Discarding response message", uniqueId) @@ -476,7 +451,7 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl return nil, errorFromValidation(endpoint, err.(validator.ValidationErrors), uniqueId, request.GetFeatureName()) } return &callResult, nil - } else if typeId == CALL_ERROR { + case CALL_ERROR, CALL_RESULT_ERROR: _, ok := pendingRequestState.GetPendingRequest(uniqueId) if !ok { log.Infof("No previous request %v sent. Discarding error message", uniqueId) @@ -510,7 +485,16 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl return nil, errorFromValidation(endpoint, err.(validator.ValidationErrors), uniqueId, "") } return &callError, nil - } else { + + case SEND: + // SEND can be only sent in OCPP 2.1 + if endpoint.Dialect() != ocpp.V21 { + return nil, ocpp.NewError(MessageTypeNotSupported, "SEND message is not supported in this OCPP version", uniqueId) + } + + // Send does not expect a confirmation, so it is not added to the pending requests. + return nil, nil + default: return nil, ocpp.NewError(MessageTypeNotSupported, fmt.Sprintf("Invalid message type ID %v", typeId), uniqueId) } } diff --git a/ocppj/parse.go b/ocppj/parse.go new file mode 100644 index 00000000..d8ad0037 --- /dev/null +++ b/ocppj/parse.go @@ -0,0 +1,69 @@ +package ocppj + +import ( + "bytes" + "encoding/json" + "github.com/lorenzodonini/ocpp-go/ocpp" + "reflect" +) + +func parseRawJsonRequest(raw interface{}, requestType reflect.Type) (ocpp.Request, error) { + if raw == nil { + raw = &struct{}{} + } + bytes, err := json.Marshal(raw) + if err != nil { + return nil, err + } + request := reflect.New(requestType).Interface() + err = json.Unmarshal(bytes, &request) + if err != nil { + return nil, err + } + result := request.(ocpp.Request) + return result, nil +} + +func parseRawJsonConfirmation(raw interface{}, confirmationType reflect.Type) (ocpp.Response, error) { + if raw == nil { + raw = &struct{}{} + } + bytes, err := json.Marshal(raw) + if err != nil { + return nil, err + } + confirmation := reflect.New(confirmationType).Interface() + err = json.Unmarshal(bytes, &confirmation) + if err != nil { + return nil, err + } + result := confirmation.(ocpp.Response) + return result, nil +} + +// 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 +} + +// Unmarshals an OCPP-J json object from a byte array. +// Returns the array of elements contained in the message. +func ParseRawJsonMessage(dataJson []byte) ([]interface{}, error) { + var arr []interface{} + err := json.Unmarshal(dataJson, &arr) + if err != nil { + return nil, err + } + return arr, nil +} + +// Unmarshals an OCPP-J json object from a JSON string. +// Returns the array of elements contained in the message. +func ParseJsonMessage(dataJson string) ([]interface{}, error) { + rawJson := []byte(dataJson) + return ParseRawJsonMessage(rawJson) +} From 91b5f05689f2f2a7ab0864f1c69a2f4461f1f4ec Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Fri, 20 Jun 2025 01:22:20 +0200 Subject: [PATCH 02/12] feat: ocpp 2.1 (wip) --- ocpp2.1/authorization/authorization.go | 24 + ocpp2.1/authorization/authorize.go | 96 ++ ocpp2.1/authorization/clear_cache.go | 82 ++ ocpp2.1/availability/availability.go | 28 + ocpp2.1/availability/change_availability.go | 105 ++ ocpp2.1/availability/heartbeat.go | 69 ++ ocpp2.1/availability/status_notification.go | 92 ++ ocpp2.1/battey_swap/battery_swap.go | 75 ++ ocpp2.1/battey_swap/profile.go | 23 + ocpp2.1/battey_swap/request_battery_swap.go | 60 + ocpp2.1/battey_swap/types.go | 28 + ocpp2.1/charging_station.go | 804 +++++++++++++ ocpp2.1/csms.go | 1049 +++++++++++++++++ ocpp2.1/data/data.go | 23 + ocpp2.1/data/data_transfer.go | 86 ++ ocpp2.1/der/clear_der_control.go | 59 + ocpp2.1/der/der.go | 31 + ocpp2.1/der/get_der_control.go | 60 + ocpp2.1/der/notify_der_alarm.go | 58 + ocpp2.1/der/notify_der_start_stop.go | 62 + ocpp2.1/der/report_der_control.go | 61 + ocpp2.1/der/set_der_control.go | 69 ++ ocpp2.1/der/types.go | 280 +++++ .../diagnostics/clear_variable_monitoring.go | 86 ++ ocpp2.1/diagnostics/customer_information.go | 89 ++ ocpp2.1/diagnostics/diagnostics.go | 51 + ocpp2.1/diagnostics/get_log.go | 111 ++ ocpp2.1/diagnostics/get_monitoring_report.go | 85 ++ .../diagnostics/log_status_notification.go | 87 ++ .../notify_customer_information.go | 62 + ocpp2.1/diagnostics/notify_event.go | 127 ++ .../diagnostics/notify_monitoring_report.go | 85 ++ ocpp2.1/diagnostics/set_monitoring_base.go | 86 ++ ocpp2.1/diagnostics/set_monitoring_level.go | 91 ++ .../diagnostics/set_variable_monitoring.go | 114 ++ ocpp2.1/diagnostics/types.go | 34 + ocpp2.1/display/clear_display_message.go | 82 ++ ocpp2.1/display/display.go | 30 + ocpp2.1/display/get_display_messages.go | 63 + ocpp2.1/display/notify_display_messages.go | 58 + ocpp2.1/display/set_display_message.go | 92 ++ ocpp2.1/display/types.go | 83 ++ ocpp2.1/firmware/firmware.go | 33 + .../firmware/firmware_status_notification.go | 95 ++ ocpp2.1/firmware/publish_firmware.go | 68 ++ .../publish_firmware_status_notification.go | 91 ++ ocpp2.1/firmware/unpublish_firmware.go | 82 ++ ocpp2.1/firmware/update_firmware.go | 106 ++ ocpp2.1/iso15118/delete_certificate.go | 82 ++ ocpp2.1/iso15118/get_15118ev_certificate.go | 88 ++ ocpp2.1/iso15118/get_certificate_status.go | 61 + .../iso15118/get_installed_certificate_ids.go | 81 ++ ocpp2.1/iso15118/install_certificate.go | 85 ++ ocpp2.1/iso15118/iso_15118.go | 37 + ocpp2.1/localauth/get_local_list_version.go | 54 + ocpp2.1/localauth/local_auth_list.go | 25 + ocpp2.1/localauth/send_local_list.go | 115 ++ ocpp2.1/meter/meter.go | 21 + ocpp2.1/meter/meter_values.go | 63 + ocpp2.1/provisioning/boot_notification.go | 137 +++ ocpp2.1/provisioning/get_base_report.go | 85 ++ ocpp2.1/provisioning/get_report.go | 85 ++ ocpp2.1/provisioning/get_variables.go | 99 ++ ocpp2.1/provisioning/notify_report.go | 151 +++ ocpp2.1/provisioning/provisioning.go | 43 + ocpp2.1/provisioning/reset.go | 109 ++ ocpp2.1/provisioning/set_network_profile.go | 226 ++++ ocpp2.1/provisioning/set_variables.go | 101 ++ ocpp2.1/remotecontrol/remote_control.go | 30 + .../request_start_transaction.go | 91 ++ .../remotecontrol/request_stop_transaction.go | 63 + ocpp2.1/remotecontrol/trigger_message.go | 116 ++ ocpp2.1/remotecontrol/unlock_connector.go | 90 ++ ocpp2.1/reservation/cancel_reservation.go | 82 ++ ocpp2.1/reservation/reservation.go | 29 + .../reservation/reservation_status_update.go | 85 ++ ocpp2.1/reservation/reserve_now.go | 140 +++ ocpp2.1/security/certificate_signed.go | 84 ++ ocpp2.1/security/security.go | 27 + .../security/security_event_notification.go | 58 + ocpp2.1/security/sign_certificate.go | 64 + .../smartcharging/clear_charging_profile.go | 91 ++ .../smartcharging/cleared_charging_limit.go | 59 + .../smartcharging/get_charging_profiles.go | 93 ++ .../smartcharging/get_composite_schedule.go | 91 ++ .../smartcharging/notify_charging_limit.go | 70 ++ .../smartcharging/notify_ev_charging_needs.go | 168 +++ .../notify_ev_charging_schedule.go | 64 + .../smartcharging/report_charging_profiles.go | 65 + ocpp2.1/smartcharging/set_charging_profile.go | 91 ++ ocpp2.1/smartcharging/smart_charging.go | 47 + .../tariffcost/change_transaction_tariff.go | 94 ++ ocpp2.1/tariffcost/clear_tariffs.go | 81 ++ ocpp2.1/tariffcost/cost_updated.go | 55 + ocpp2.1/tariffcost/get_tariffs.go | 67 ++ ocpp2.1/tariffcost/set_default_tariff.go | 92 ++ ocpp2.1/tariffcost/tariff_cost.go | 24 + ocpp2.1/tariffcost/types.go | 49 + .../transactions/get_transaction_status.go | 57 + ocpp2.1/transactions/transaction_event.go | 99 ++ ocpp2.1/transactions/transactions.go | 24 + ocpp2.1/transactions/types.go | 154 +++ ocpp2.1/types/authorization.go | 105 ++ ocpp2.1/types/certificates.go | 130 ++ ocpp2.1/types/cost.go | 76 ++ ocpp2.1/types/datetime.go | 81 ++ ocpp2.1/types/measurements.go | 182 +++ ocpp2.1/types/smart_charging.go | 193 +++ ocpp2.1/types/tariff.go | 155 +++ ocpp2.1/types/transaction.go | 21 + ocpp2.1/types/types.go | 199 ++++ ocpp2.1/v21.go | 457 +++++++ ocpp2.1/v2x/affr_signal.go | 57 + ocpp2.1/v2x/notify_allowed_energy_transfer.go | 57 + ocpp2.1/v2x/v2x.go | 23 + 115 files changed, 11393 insertions(+) create mode 100644 ocpp2.1/authorization/authorization.go create mode 100644 ocpp2.1/authorization/authorize.go create mode 100644 ocpp2.1/authorization/clear_cache.go create mode 100644 ocpp2.1/availability/availability.go create mode 100644 ocpp2.1/availability/change_availability.go create mode 100644 ocpp2.1/availability/heartbeat.go create mode 100644 ocpp2.1/availability/status_notification.go create mode 100644 ocpp2.1/battey_swap/battery_swap.go create mode 100644 ocpp2.1/battey_swap/profile.go create mode 100644 ocpp2.1/battey_swap/request_battery_swap.go create mode 100644 ocpp2.1/battey_swap/types.go create mode 100644 ocpp2.1/charging_station.go create mode 100644 ocpp2.1/csms.go create mode 100644 ocpp2.1/data/data.go create mode 100644 ocpp2.1/data/data_transfer.go create mode 100644 ocpp2.1/der/clear_der_control.go create mode 100644 ocpp2.1/der/der.go create mode 100644 ocpp2.1/der/get_der_control.go create mode 100644 ocpp2.1/der/notify_der_alarm.go create mode 100644 ocpp2.1/der/notify_der_start_stop.go create mode 100644 ocpp2.1/der/report_der_control.go create mode 100644 ocpp2.1/der/set_der_control.go create mode 100644 ocpp2.1/der/types.go create mode 100644 ocpp2.1/diagnostics/clear_variable_monitoring.go create mode 100644 ocpp2.1/diagnostics/customer_information.go create mode 100644 ocpp2.1/diagnostics/diagnostics.go create mode 100644 ocpp2.1/diagnostics/get_log.go create mode 100644 ocpp2.1/diagnostics/get_monitoring_report.go create mode 100644 ocpp2.1/diagnostics/log_status_notification.go create mode 100644 ocpp2.1/diagnostics/notify_customer_information.go create mode 100644 ocpp2.1/diagnostics/notify_event.go create mode 100644 ocpp2.1/diagnostics/notify_monitoring_report.go create mode 100644 ocpp2.1/diagnostics/set_monitoring_base.go create mode 100644 ocpp2.1/diagnostics/set_monitoring_level.go create mode 100644 ocpp2.1/diagnostics/set_variable_monitoring.go create mode 100644 ocpp2.1/diagnostics/types.go create mode 100644 ocpp2.1/display/clear_display_message.go create mode 100644 ocpp2.1/display/display.go create mode 100644 ocpp2.1/display/get_display_messages.go create mode 100644 ocpp2.1/display/notify_display_messages.go create mode 100644 ocpp2.1/display/set_display_message.go create mode 100644 ocpp2.1/display/types.go create mode 100644 ocpp2.1/firmware/firmware.go create mode 100644 ocpp2.1/firmware/firmware_status_notification.go create mode 100644 ocpp2.1/firmware/publish_firmware.go create mode 100644 ocpp2.1/firmware/publish_firmware_status_notification.go create mode 100644 ocpp2.1/firmware/unpublish_firmware.go create mode 100644 ocpp2.1/firmware/update_firmware.go create mode 100644 ocpp2.1/iso15118/delete_certificate.go create mode 100644 ocpp2.1/iso15118/get_15118ev_certificate.go create mode 100644 ocpp2.1/iso15118/get_certificate_status.go create mode 100644 ocpp2.1/iso15118/get_installed_certificate_ids.go create mode 100644 ocpp2.1/iso15118/install_certificate.go create mode 100644 ocpp2.1/iso15118/iso_15118.go create mode 100644 ocpp2.1/localauth/get_local_list_version.go create mode 100644 ocpp2.1/localauth/local_auth_list.go create mode 100644 ocpp2.1/localauth/send_local_list.go create mode 100644 ocpp2.1/meter/meter.go create mode 100644 ocpp2.1/meter/meter_values.go create mode 100644 ocpp2.1/provisioning/boot_notification.go create mode 100644 ocpp2.1/provisioning/get_base_report.go create mode 100644 ocpp2.1/provisioning/get_report.go create mode 100644 ocpp2.1/provisioning/get_variables.go create mode 100644 ocpp2.1/provisioning/notify_report.go create mode 100644 ocpp2.1/provisioning/provisioning.go create mode 100644 ocpp2.1/provisioning/reset.go create mode 100644 ocpp2.1/provisioning/set_network_profile.go create mode 100644 ocpp2.1/provisioning/set_variables.go create mode 100644 ocpp2.1/remotecontrol/remote_control.go create mode 100644 ocpp2.1/remotecontrol/request_start_transaction.go create mode 100644 ocpp2.1/remotecontrol/request_stop_transaction.go create mode 100644 ocpp2.1/remotecontrol/trigger_message.go create mode 100644 ocpp2.1/remotecontrol/unlock_connector.go create mode 100644 ocpp2.1/reservation/cancel_reservation.go create mode 100644 ocpp2.1/reservation/reservation.go create mode 100644 ocpp2.1/reservation/reservation_status_update.go create mode 100644 ocpp2.1/reservation/reserve_now.go create mode 100644 ocpp2.1/security/certificate_signed.go create mode 100644 ocpp2.1/security/security.go create mode 100644 ocpp2.1/security/security_event_notification.go create mode 100644 ocpp2.1/security/sign_certificate.go create mode 100644 ocpp2.1/smartcharging/clear_charging_profile.go create mode 100644 ocpp2.1/smartcharging/cleared_charging_limit.go create mode 100644 ocpp2.1/smartcharging/get_charging_profiles.go create mode 100644 ocpp2.1/smartcharging/get_composite_schedule.go create mode 100644 ocpp2.1/smartcharging/notify_charging_limit.go create mode 100644 ocpp2.1/smartcharging/notify_ev_charging_needs.go create mode 100644 ocpp2.1/smartcharging/notify_ev_charging_schedule.go create mode 100644 ocpp2.1/smartcharging/report_charging_profiles.go create mode 100644 ocpp2.1/smartcharging/set_charging_profile.go create mode 100644 ocpp2.1/smartcharging/smart_charging.go create mode 100644 ocpp2.1/tariffcost/change_transaction_tariff.go create mode 100644 ocpp2.1/tariffcost/clear_tariffs.go create mode 100644 ocpp2.1/tariffcost/cost_updated.go create mode 100644 ocpp2.1/tariffcost/get_tariffs.go create mode 100644 ocpp2.1/tariffcost/set_default_tariff.go create mode 100644 ocpp2.1/tariffcost/tariff_cost.go create mode 100644 ocpp2.1/tariffcost/types.go create mode 100644 ocpp2.1/transactions/get_transaction_status.go create mode 100644 ocpp2.1/transactions/transaction_event.go create mode 100644 ocpp2.1/transactions/transactions.go create mode 100644 ocpp2.1/transactions/types.go create mode 100644 ocpp2.1/types/authorization.go create mode 100644 ocpp2.1/types/certificates.go create mode 100644 ocpp2.1/types/cost.go create mode 100644 ocpp2.1/types/datetime.go create mode 100644 ocpp2.1/types/measurements.go create mode 100644 ocpp2.1/types/smart_charging.go create mode 100644 ocpp2.1/types/tariff.go create mode 100644 ocpp2.1/types/transaction.go create mode 100644 ocpp2.1/types/types.go create mode 100644 ocpp2.1/v21.go create mode 100644 ocpp2.1/v2x/affr_signal.go create mode 100644 ocpp2.1/v2x/notify_allowed_energy_transfer.go create mode 100644 ocpp2.1/v2x/v2x.go diff --git a/ocpp2.1/authorization/authorization.go b/ocpp2.1/authorization/authorization.go new file mode 100644 index 00000000..6ac58022 --- /dev/null +++ b/ocpp2.1/authorization/authorization.go @@ -0,0 +1,24 @@ +// The authorization functional block contains OCPP 2.1 authorization-related features. It contains different ways of authorizing a user, online and/or offline. +package authorization + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Authorization profile. +type CSMSHandler interface { + // OnAuthorize is called on the CSMS whenever an AuthorizeRequest is received from a charging station. + OnAuthorize(chargingStationID string, request *AuthorizeRequest) (confirmation *AuthorizeResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Authorization profile. +type ChargingStationHandler interface { + // OnClearCache is called on a charging station whenever a ClearCacheRequest is received from the CSMS. + OnClearCache(request *ClearCacheRequest) (confirmation *ClearCacheResponse, err error) +} + +const ProfileName = "authorization" + +var Profile = ocpp.NewProfile( + ProfileName, + AuthorizeFeature{}, + ClearCacheFeature{}, +) diff --git a/ocpp2.1/authorization/authorize.go b/ocpp2.1/authorization/authorize.go new file mode 100644 index 00000000..a51c3d8b --- /dev/null +++ b/ocpp2.1/authorization/authorize.go @@ -0,0 +1,96 @@ +package authorization + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Authorize (CS -> CSMS) -------------------- + +const AuthorizeFeatureName = "Authorize" + +// The Certificate status information. +type AuthorizeCertificateStatus string + +const ( + CertificateStatusAccepted AuthorizeCertificateStatus = "Accepted" + CertificateStatusSignatureError AuthorizeCertificateStatus = "SignatureError" + CertificateStatusCertificateExpired AuthorizeCertificateStatus = "CertificateExpired" + CertificateStatusCertificateRevoked AuthorizeCertificateStatus = "CertificateRevoked" + CertificateStatusNoCertificateAvailable AuthorizeCertificateStatus = "NoCertificateAvailable" + CertificateStatusCertChainError AuthorizeCertificateStatus = "CertChainError" + CertificateStatusContractCancelled AuthorizeCertificateStatus = "ContractCancelled" +) + +func isValidAuthorizeCertificateStatus(fl validator.FieldLevel) bool { + status := AuthorizeCertificateStatus(fl.Field().String()) + switch status { + case CertificateStatusAccepted, CertificateStatusCertChainError, CertificateStatusCertificateExpired, CertificateStatusSignatureError, CertificateStatusNoCertificateAvailable, CertificateStatusCertificateRevoked, CertificateStatusContractCancelled: + return true + default: + return false + } +} + +// The field definition of the Authorize request payload sent by the Charging Station to the CSMS. +type AuthorizeRequest struct { + Certificate *string `json:"certificate,omitempty" validate:"max=10000"` + IdToken types.IdToken `json:"idToken" validate:"required"` + CertificateHashData []types.OCSPRequestDataType `json:"iso15118CertificateHashData,omitempty" validate:"max=4,dive"` +} + +// This field definition of the Authorize response payload, sent by the Charging Station to the CSMS in response to an AuthorizeRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type AuthorizeResponse struct { + CertificateStatus AuthorizeCertificateStatus `json:"certificateStatus,omitempty" validate:"omitempty,authorizeCertificateStatus21"` + AllowedEnergyTransfer []types.EnergyTransferMode `json:"allowedEnergyTransfer,omitempty" validate:"omitempty,energyTransferMode"` + IdTokenInfo types.IdTokenInfo `json:"idTokenInfo" validate:"required"` + Tariff *types.Tariff `json:"tariff,omitempty" validate:"omitempty,dive"` +} + +// Before the owner of an electric vehicle can start or stop charging, the Charging Station has to authorize the operation. +// Upon receipt of an AuthorizeRequest, the CSMS SHALL respond with an AuthorizeResponse. +// This response payload SHALL indicate whether or not the idTag is accepted by the CSMS. +// If the CSMS accepts the idToken then the response payload MUST include an authorization status value indicating acceptance or a reason for rejection. +// +// A Charging Station MAY authorize identifier locally without involving the CSMS, as described in Local Authorization List. +// +// The Charging Station SHALL only supply energy after authorization. +type AuthorizeFeature struct{} + +func (f AuthorizeFeature) GetFeatureName() string { + return AuthorizeFeatureName +} + +func (f AuthorizeFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(AuthorizeRequest{}) +} + +func (f AuthorizeFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(AuthorizeResponse{}) +} + +func (r AuthorizeRequest) GetFeatureName() string { + return AuthorizeFeatureName +} + +func (c AuthorizeResponse) GetFeatureName() string { + return AuthorizeFeatureName +} + +// Creates a new AuthorizeRequest, containing all required fields. There are no optional fields for this message. +func NewAuthorizationRequest(idToken string, tokenType types.IdTokenType) *AuthorizeRequest { + return &AuthorizeRequest{IdToken: types.IdToken{IdToken: idToken, Type: tokenType}} +} + +// Creates a new AuthorizeResponse. There are no optional fields for this message. +func NewAuthorizationResponse(idTokenInfo types.IdTokenInfo) *AuthorizeResponse { + return &AuthorizeResponse{IdTokenInfo: idTokenInfo} +} + +func init() { + _ = types.Validate.RegisterValidation("authorizeCertificateStatus21", isValidAuthorizeCertificateStatus) +} diff --git a/ocpp2.1/authorization/clear_cache.go b/ocpp2.1/authorization/clear_cache.go new file mode 100644 index 00000000..665559ec --- /dev/null +++ b/ocpp2.1/authorization/clear_cache.go @@ -0,0 +1,82 @@ +package authorization + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Clear Cache (CSMS -> CS) -------------------- + +const ClearCacheFeatureName = "ClearCache" + +// Status returned in response to ClearCacheRequest. +type ClearCacheStatus string + +const ( + ClearCacheStatusAccepted ClearCacheStatus = "Accepted" + ClearCacheStatusRejected ClearCacheStatus = "Rejected" +) + +func isValidClearCacheStatus(fl validator.FieldLevel) bool { + status := ClearCacheStatus(fl.Field().String()) + switch status { + case ClearCacheStatusAccepted, ClearCacheStatusRejected: + return true + default: + return false + } +} + +// The field definition of the ClearCache request payload sent by the CSMS to the Charging Station. +type ClearCacheRequest struct { +} + +// This field definition of the ClearCache response payload, sent by the Charging Station to the CSMS in response to a ClearCacheRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClearCacheResponse struct { + Status ClearCacheStatus `json:"status" validate:"required,cacheStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// CSMS can request a Charging Station to clear its Authorization Cache. +// The CSMS SHALL send a ClearCacheRequest payload for clearing the Charging Station’s Authorization Cache. +// Upon receipt of a ClearCacheRequest, the Charging Station SHALL respond with a ClearCacheResponse payload. +// The response payload SHALL indicate whether the Charging Station was able to clear its Authorization Cache. +type ClearCacheFeature struct{} + +func (f ClearCacheFeature) GetFeatureName() string { + return ClearCacheFeatureName +} + +func (f ClearCacheFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearCacheRequest{}) +} + +func (f ClearCacheFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearCacheResponse{}) +} + +func (r ClearCacheRequest) GetFeatureName() string { + return ClearCacheFeatureName +} + +func (c ClearCacheResponse) GetFeatureName() string { + return ClearCacheFeatureName +} + +// Creates a new ClearCacheRequest, which doesn't contain any required or optional fields. +func NewClearCacheRequest() *ClearCacheRequest { + return &ClearCacheRequest{} +} + +// Creates a new ClearCacheResponse, containing all required fields. There are no optional fields for this message. +func NewClearCacheResponse(status ClearCacheStatus) *ClearCacheResponse { + return &ClearCacheResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("cacheStatus21", isValidClearCacheStatus) +} diff --git a/ocpp2.1/availability/availability.go b/ocpp2.1/availability/availability.go new file mode 100644 index 00000000..a903d258 --- /dev/null +++ b/ocpp2.1/availability/availability.go @@ -0,0 +1,28 @@ +// The availability functional block contains OCPP 2.0 features for notifying the CSMS of availability and status changes. +// A CSMS can also instruct a charging station to change its availability. +package availability + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Availability profile. +type CSMSHandler interface { + // OnHeartbeat is called on the CSMS whenever a HeartbeatResponse is received from a charging station. + OnHeartbeat(chargingStationID string, request *HeartbeatRequest) (response *HeartbeatResponse, err error) + // OnStatusNotification is called on the CSMS whenever a StatusNotificationRequest is received from a charging station. + OnStatusNotification(chargingStationID string, request *StatusNotificationRequest) (response *StatusNotificationResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Availability profile. +type ChargingStationHandler interface { + // OnChangeAvailability is called on a charging station whenever a ChangeAvailabilityRequest is received from the CSMS. + OnChangeAvailability(request *ChangeAvailabilityRequest) (response *ChangeAvailabilityResponse, err error) +} + +const ProfileName = "Availability" + +var Profile = ocpp.NewProfile( + ProfileName, + ChangeAvailabilityFeature{}, + HeartbeatFeature{}, + StatusNotificationFeature{}, +) diff --git a/ocpp2.1/availability/change_availability.go b/ocpp2.1/availability/change_availability.go new file mode 100644 index 00000000..0cbf2916 --- /dev/null +++ b/ocpp2.1/availability/change_availability.go @@ -0,0 +1,105 @@ +package availability + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Change Availability (CSMS -> CS) -------------------- + +const ChangeAvailabilityFeatureName = "ChangeAvailability" + +// Requested availability change in ChangeAvailabilityRequest. +type OperationalStatus string + +const ( + OperationalStatusInoperative OperationalStatus = "Inoperative" + OperationalStatusOperative OperationalStatus = "Operative" +) + +func isValidOperationalStatus(fl validator.FieldLevel) bool { + status := OperationalStatus(fl.Field().String()) + switch status { + case OperationalStatusInoperative, OperationalStatusOperative: + return true + default: + return false + } +} + +// Status returned in response to ChangeAvailabilityRequest +type ChangeAvailabilityStatus string + +const ( + ChangeAvailabilityStatusAccepted ChangeAvailabilityStatus = "Accepted" + ChangeAvailabilityStatusRejected ChangeAvailabilityStatus = "Rejected" + ChangeAvailabilityStatusScheduled ChangeAvailabilityStatus = "Scheduled" +) + +func isValidChangeAvailabilityStatus(fl validator.FieldLevel) bool { + status := ChangeAvailabilityStatus(fl.Field().String()) + switch status { + case ChangeAvailabilityStatusAccepted, ChangeAvailabilityStatusRejected, ChangeAvailabilityStatusScheduled: + return true + default: + return false + } +} + +// The field definition of the ChangeAvailability request payload sent by the CSMS to the Charging Station. +type ChangeAvailabilityRequest struct { + OperationalStatus OperationalStatus `json:"operationalStatus" validate:"required,operationalStatus21"` + Evse *types.EVSE `json:"evse,omitempty" validate:"omitempty"` +} + +// This field definition of the ChangeAvailability response payload, sent by the Charging Station to the CSMS in response to a ChangeAvailabilityRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ChangeAvailabilityResponse struct { + Status ChangeAvailabilityStatus `json:"status" validate:"required,changeAvailabilityStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// CSMS can request a Charging Station to change its availability. +// A Charging Station is considered available (“operative”) when it is charging or ready for charging. +// A Charging Station is considered unavailable when it does not allow any charging. +// The CSMS SHALL send a ChangeAvailabilityRequest for requesting a Charging Station to change its availability. +// The CSMS can change the availability to available or unavailable. +type ChangeAvailabilityFeature struct{} + +func (f ChangeAvailabilityFeature) GetFeatureName() string { + return ChangeAvailabilityFeatureName +} + +func (f ChangeAvailabilityFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ChangeAvailabilityRequest{}) +} + +func (f ChangeAvailabilityFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ChangeAvailabilityResponse{}) +} + +func (r ChangeAvailabilityRequest) GetFeatureName() string { + return ChangeAvailabilityFeatureName +} + +func (c ChangeAvailabilityResponse) GetFeatureName() string { + return ChangeAvailabilityFeatureName +} + +// Creates a new ChangeAvailabilityRequest, containing all required fields. Optional fields may be set afterwards. +func NewChangeAvailabilityRequest(operationalStatus OperationalStatus) *ChangeAvailabilityRequest { + return &ChangeAvailabilityRequest{OperationalStatus: operationalStatus} +} + +// Creates a new ChangeAvailabilityResponse, containing all required fields. Optional fields may be set afterwards. +func NewChangeAvailabilityResponse(status ChangeAvailabilityStatus) *ChangeAvailabilityResponse { + return &ChangeAvailabilityResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("operationalStatus21", isValidOperationalStatus) + _ = types.Validate.RegisterValidation("changeAvailabilityStatus21", isValidChangeAvailabilityStatus) +} diff --git a/ocpp2.1/availability/heartbeat.go b/ocpp2.1/availability/heartbeat.go new file mode 100644 index 00000000..46dcf44b --- /dev/null +++ b/ocpp2.1/availability/heartbeat.go @@ -0,0 +1,69 @@ +package availability + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Heartbeat (CS -> CSMS) -------------------- + +const HeartbeatFeatureName = "Heartbeat" + +// The field definition of the Heartbeat request payload sent by the Charging Station to the CSMS. +type HeartbeatRequest struct { +} + +// This field definition of the Heartbeat response payload, sent by the CSMS to the Charging Station in response to a HeartbeatRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type HeartbeatResponse struct { + CurrentTime types.DateTime `json:"currentTime" validate:"required"` +} + +// A Charging Station may send a heartbeat to let the CSMS know the Charging Station is still connected, after a configurable time interval. +// +// Upon receipt of HeartbeatRequest, the CSMS responds with HeartbeatResponse. +// The response message contains the current time of the CSMS, which the Charging Station MAY use to synchronize its internal clock. +type HeartbeatFeature struct{} + +func (f HeartbeatFeature) GetFeatureName() string { + return HeartbeatFeatureName +} + +func (f HeartbeatFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(HeartbeatRequest{}) +} + +func (f HeartbeatFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(HeartbeatResponse{}) +} + +func (r HeartbeatRequest) GetFeatureName() string { + return HeartbeatFeatureName +} + +func (c HeartbeatResponse) GetFeatureName() string { + return HeartbeatFeatureName +} + +// Creates a new HeartbeatRequest, which doesn't contain any required or optional fields. +func NewHeartbeatRequest() *HeartbeatRequest { + return &HeartbeatRequest{} +} + +// Creates a new HeartbeatResponse, containing all required fields. There are no optional fields for this message. +func NewHeartbeatResponse(currentTime types.DateTime) *HeartbeatResponse { + return &HeartbeatResponse{CurrentTime: currentTime} +} + +func validateHeartbeatResponse(sl validator.StructLevel) { + response := sl.Current().Interface().(HeartbeatResponse) + if types.DateTimeIsNull(&response.CurrentTime) { + sl.ReportError(response.CurrentTime, "CurrentTime", "currentTime", "required", "") + } +} + +func init() { + types.Validate.RegisterStructValidation(validateHeartbeatResponse, HeartbeatResponse{}) +} diff --git a/ocpp2.1/availability/status_notification.go b/ocpp2.1/availability/status_notification.go new file mode 100644 index 00000000..9c24f9c4 --- /dev/null +++ b/ocpp2.1/availability/status_notification.go @@ -0,0 +1,92 @@ +package availability + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Status Notification (CS -> CSMS) -------------------- + +const StatusNotificationFeatureName = "StatusNotification" + +type ConnectorStatus string + +const ( + ConnectorStatusAvailable ConnectorStatus = "Available" // When a Connector becomes available for a new User (Operative) + ConnectorStatusOccupied ConnectorStatus = "Occupied" // When a Connector becomes occupied, so it is not available for a new EV driver. (Operative) + ConnectorStatusReserved ConnectorStatus = "Reserved" // When a Connector becomes reserved as a result of ReserveNow command (Operative) + ConnectorStatusUnavailable ConnectorStatus = "Unavailable" // When a Connector becomes unavailable as the result of a Change Availability command or an event upon which the Charging Station transitions to unavailable at its discretion. + ConnectorStatusFaulted ConnectorStatus = "Faulted" // When a Connector (or the EVSE or the entire Charging Station it belongs to) has reported an error and is not available for energy delivery. (Inoperative). +) + +func isValidConnectorStatus(fl validator.FieldLevel) bool { + status := ConnectorStatus(fl.Field().String()) + switch status { + case ConnectorStatusAvailable, ConnectorStatusOccupied, ConnectorStatusReserved, ConnectorStatusUnavailable, ConnectorStatusFaulted: + return true + default: + return false + } +} + +// The field definition of the StatusNotification request payload sent by the Charging Station to the CSMS. +type StatusNotificationRequest struct { + Timestamp *types.DateTime `json:"timestamp" validate:"required"` + ConnectorStatus ConnectorStatus `json:"connectorStatus" validate:"required,connectorStatus21"` + EvseID int `json:"evseId" validate:"gte=0"` + ConnectorID int `json:"connectorId" validate:"gte=0"` +} + +// This field definition of the StatusNotification response payload, sent by the CSMS to the Charging Station in response to a StatusNotificationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type StatusNotificationResponse struct { +} + +// The Charging Station notifies the CSMS about a connector status change. +// This may typically be after on of the following events: +// - (re)boot +// - reset +// - any transaction event (start/stop/authorization) +// - reservation events +// - change availability operations +// - remote triggers +// +// The charging station sends a StatusNotificationRequest to the CSMS with information about the new status. +// The CSMS responds with a StatusNotificationResponse. +type StatusNotificationFeature struct{} + +func (f StatusNotificationFeature) GetFeatureName() string { + return StatusNotificationFeatureName +} + +func (f StatusNotificationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(StatusNotificationRequest{}) +} + +func (f StatusNotificationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(StatusNotificationResponse{}) +} + +func (r StatusNotificationRequest) GetFeatureName() string { + return StatusNotificationFeatureName +} + +func (c StatusNotificationResponse) GetFeatureName() string { + return StatusNotificationFeatureName +} + +// Creates a new StatusNotificationRequest, containing all required fields. There are no optional fields for this message. +func NewStatusNotificationRequest(timestamp *types.DateTime, status ConnectorStatus, evseID int, connectorID int) *StatusNotificationRequest { + return &StatusNotificationRequest{Timestamp: timestamp, ConnectorStatus: status, EvseID: evseID, ConnectorID: connectorID} +} + +// Creates a new StatusNotificationResponse, which doesn't contain any required or optional fields. +func NewStatusNotificationResponse() *StatusNotificationResponse { + return &StatusNotificationResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("connectorStatus21", isValidConnectorStatus) +} diff --git a/ocpp2.1/battey_swap/battery_swap.go b/ocpp2.1/battey_swap/battery_swap.go new file mode 100644 index 00000000..ee4152be --- /dev/null +++ b/ocpp2.1/battey_swap/battery_swap.go @@ -0,0 +1,75 @@ +package battey_swap + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- BatterySwap (CS -> CSMS) -------------------- + +const BatterySwap = "BatterySwap" + +// The field definition of the BatterySwapRequest request payload sent by the CSMS to the Charging Station. +type BatterySwapRequest struct { + EventType BatterSwapEvent `json:"eventType" validate:"required,batterySwapEvent"` + RequestId int `json:"requestId" validate:"required"` // Unique identifier for the request + IdToken types.IdTokenType `json:"idToken" validate:"required,dive"` // Optional field for the ID token of the user + BatteryData BatteryData `json:"batteryData" validate:"required,dive"` // Contains information about the battery to be swapped +} + +type BatteryData struct { + EvseId int `json:"evseId" validate:"required,gte=1"` // The ID of the EVSE where the battery swap is taking place + SerialNumber string `json:"serialNumber" validate:"required,max=50"` // The serial number of the battery being swapped + SoC float64 `json:"soC" validate:"required,gte=0,lte=100"` + SoH float64 `json:"soH" validate:"required,gte=0,lte=100"` + ProductionDate *types.DateTime `json:"productionDate" validate:"omitempty"` + VendorInfo string `json:"vendorInfo" validate:"omitempty,max=500"` +} + +// This field definition of the BatterySwapResponse +type BatterySwapResponse struct { +} + +type BatterySwapFeature struct{} + +func (f BatterySwapFeature) GetFeatureName() string { + return BatterySwap +} + +func (f BatterySwapFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(BatterySwapRequest{}) +} + +func (f BatterySwapFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(BatterySwapResponse{}) +} + +func (r BatterySwapRequest) GetFeatureName() string { + return BatterySwap +} + +func (c BatterySwapResponse) GetFeatureName() string { + return BatterySwap +} + +func NewBatterySwapRequest(eventType BatterSwapEvent, requestId int, idToken types.IdTokenType, batteryData BatteryData) BatterySwapRequest { + return BatterySwapRequest{ + EventType: eventType, + RequestId: requestId, + IdToken: idToken, + BatteryData: batteryData, + } +} + +func NewBatteryData(evseId int, serialNumber string, soc, soh float64) BatteryData { + return BatteryData{ + EvseId: evseId, + SerialNumber: serialNumber, + SoC: soc, + SoH: soh, + } +} + +func NewBatterySwapResponse() BatterySwapResponse { + return BatterySwapResponse{} +} diff --git a/ocpp2.1/battey_swap/profile.go b/ocpp2.1/battey_swap/profile.go new file mode 100644 index 00000000..e91b2ce7 --- /dev/null +++ b/ocpp2.1/battey_swap/profile.go @@ -0,0 +1,23 @@ +package battey_swap + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" +) + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Battery Swap. +type CSMSHandler interface { + OnBatterySwap(chargingStationID string, request *BatterySwapRequest) (*BatterySwapResponse, error) +} + +// Needs to be implemented by Charging stations for handling messages part of the Battery Swap. +type ChargingStationHandler interface { + OnRequestBatterySwap(chargingStationID string, request *RequestBatterySwapRequest) (*RequestBatterySwapResponse, error) +} + +const ProfileName = "BatterySwap" + +var Profile = ocpp.NewProfile( + ProfileName, + BatterySwapFeature{}, + RequestBatterySwapFeature{}, +) diff --git a/ocpp2.1/battey_swap/request_battery_swap.go b/ocpp2.1/battey_swap/request_battery_swap.go new file mode 100644 index 00000000..d6baf7f6 --- /dev/null +++ b/ocpp2.1/battey_swap/request_battery_swap.go @@ -0,0 +1,60 @@ +package battey_swap + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- RequestBatterySwap (CSMS -> CS) -------------------- + +const RequestBatterySwap = "RequestBatterySwap" + +// The field definition of the RequestBatterySwapRequest request payload sent by the CSMS to the Charging Station. +type RequestBatterySwapRequest struct { + RequestId int `json:"requestId" validate:"required"` + IdToken types.IdTokenType `json:"idToken" validate:"required,dive"` +} + +// This field definition of the RequestBatterySwapResponse +type RequestBatterySwapResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +type RequestBatterySwapFeature struct{} + +func (f RequestBatterySwapFeature) GetFeatureName() string { + return RequestBatterySwap +} + +func (f RequestBatterySwapFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(RequestBatterySwapRequest{}) +} + +func (f RequestBatterySwapFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(RequestBatterySwapResponse{}) +} + +func (r RequestBatterySwapRequest) GetFeatureName() string { + return RequestBatterySwap +} + +func (c RequestBatterySwapResponse) GetFeatureName() string { + return RequestBatterySwap +} + +// Creates a new RequestBatterySwapRequest, containing all required fields. Optional fields may be set afterwards. +func NewRequestBatterySwapRequest(requestId int, idToken types.IdTokenType) *RequestBatterySwapRequest { + return &RequestBatterySwapRequest{ + RequestId: requestId, + IdToken: idToken, + } +} + +// Creates a new RequestBatterySwapResponse, containing all required fields. Optional fields may be set afterwards. +func NewRequestBatterySwapResponse(status types.GenericStatus, statusInfo *types.StatusInfo) *RequestBatterySwapResponse { + return &RequestBatterySwapResponse{ + Status: status, + StatusInfo: statusInfo, + } +} diff --git a/ocpp2.1/battey_swap/types.go b/ocpp2.1/battey_swap/types.go new file mode 100644 index 00000000..f782728f --- /dev/null +++ b/ocpp2.1/battey_swap/types.go @@ -0,0 +1,28 @@ +package battey_swap + +import ( + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" +) + +type BatterSwapEvent string + +const ( + BatterSwapEventBatteryIn BatterSwapEvent = "BatteryIn" + BatterSwapEventBatteryOut BatterSwapEvent = "BatteryOut" + BatterSwapEventBatteryOutTimeout BatterSwapEvent = "BatteryOutTimeout" +) + +func isValidBatterSwapEvent(fl validator.FieldLevel) bool { + event := fl.Field().String() + switch event { + case string(BatterSwapEventBatteryIn), string(BatterSwapEventBatteryOut), string(BatterSwapEventBatteryOutTimeout): + return true + default: + return false + } +} + +func init() { + _ = ocppj.Validate.RegisterValidation("batterySwapEvent", isValidBatterSwapEvent) +} diff --git a/ocpp2.1/charging_station.go b/ocpp2.1/charging_station.go new file mode 100644 index 00000000..d6af3d32 --- /dev/null +++ b/ocpp2.1/charging_station.go @@ -0,0 +1,804 @@ +package ocpp21 + +import ( + "fmt" + "reflect" + + "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/authorization" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/data" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/diagnostics" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/display" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/iso15118" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/meter" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/remotecontrol" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/security" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/tariffcost" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/transactions" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +type chargingStation struct { + client *ocppj.Client + securityHandler security.ChargingStationHandler + provisioningHandler provisioning.ChargingStationHandler + authorizationHandler authorization.ChargingStationHandler + localAuthListHandler localauth.ChargingStationHandler + transactionsHandler transactions.ChargingStationHandler + remoteControlHandler remotecontrol.ChargingStationHandler + availabilityHandler availability.ChargingStationHandler + reservationHandler reservation.ChargingStationHandler + tariffCostHandler tariffcost.ChargingStationHandler + meterHandler meter.ChargingStationHandler + smartChargingHandler smartcharging.ChargingStationHandler + firmwareHandler firmware.ChargingStationHandler + iso15118Handler iso15118.ChargingStationHandler + diagnosticsHandler diagnostics.ChargingStationHandler + displayHandler display.ChargingStationHandler + dataHandler data.ChargingStationHandler + responseHandler chan ocpp.Response + errorHandler chan error + callbacks callbackqueue.CallbackQueue + stopC chan struct{} + errC chan error // external error channel +} + +func (cs *chargingStation) error(err error) { + if cs.errC != nil { + cs.errC <- err + } +} + +// Errors returns a channel for error messages. If it doesn't exist it es created. +func (cs *chargingStation) Errors() <-chan error { + if cs.errC == nil { + cs.errC = make(chan error, 1) + } + return cs.errC +} + +// Callback invoked whenever a queued request is canceled, due to timeout. +// By default, the callback returns a GenericError to the caller, who sent the original request. +func (cs *chargingStation) onRequestTimeout(_ string, _ ocpp.Request, err *ocpp.Error) { + cs.errorHandler <- err +} + +func (cs *chargingStation) BootNotification(reason provisioning.BootReason, model string, vendor string, props ...func(request *provisioning.BootNotificationRequest)) (*provisioning.BootNotificationResponse, error) { + request := provisioning.NewBootNotificationRequest(reason, model, vendor) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*provisioning.BootNotificationResponse), err + } +} + +func (cs *chargingStation) Authorize(idToken string, tokenType types.IdTokenType, props ...func(request *authorization.AuthorizeRequest)) (*authorization.AuthorizeResponse, error) { + request := authorization.NewAuthorizationRequest(idToken, tokenType) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*authorization.AuthorizeResponse), err + } +} + +func (cs *chargingStation) ClearedChargingLimit(chargingLimitSource types.ChargingLimitSourceType, props ...func(request *smartcharging.ClearedChargingLimitRequest)) (*smartcharging.ClearedChargingLimitResponse, error) { + request := smartcharging.NewClearedChargingLimitRequest(chargingLimitSource) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*smartcharging.ClearedChargingLimitResponse), err + } +} + +func (cs *chargingStation) DataTransfer(vendorId string, props ...func(request *data.DataTransferRequest)) (*data.DataTransferResponse, error) { + request := data.NewDataTransferRequest(vendorId) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*data.DataTransferResponse), err + } +} + +func (cs *chargingStation) FirmwareStatusNotification(status firmware.FirmwareStatus, props ...func(request *firmware.FirmwareStatusNotificationRequest)) (*firmware.FirmwareStatusNotificationResponse, error) { + request := firmware.NewFirmwareStatusNotificationRequest(status) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*firmware.FirmwareStatusNotificationResponse), err + } +} + +func (cs *chargingStation) Get15118EVCertificate(schemaVersion string, action iso15118.CertificateAction, exiRequest string, props ...func(request *iso15118.Get15118EVCertificateRequest)) (*iso15118.Get15118EVCertificateResponse, error) { + request := iso15118.NewGet15118EVCertificateRequest(schemaVersion, action, exiRequest) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*iso15118.Get15118EVCertificateResponse), err + } +} + +func (cs *chargingStation) GetCertificateStatus(ocspRequestData types.OCSPRequestDataType, props ...func(request *iso15118.GetCertificateStatusRequest)) (*iso15118.GetCertificateStatusResponse, error) { + request := iso15118.NewGetCertificateStatusRequest(ocspRequestData) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*iso15118.GetCertificateStatusResponse), err + } +} + +func (cs *chargingStation) Heartbeat(props ...func(request *availability.HeartbeatRequest)) (*availability.HeartbeatResponse, error) { + request := availability.NewHeartbeatRequest() + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*availability.HeartbeatResponse), err + } +} + +func (cs *chargingStation) LogStatusNotification(status diagnostics.UploadLogStatus, requestID int, props ...func(request *diagnostics.LogStatusNotificationRequest)) (*diagnostics.LogStatusNotificationResponse, error) { + request := diagnostics.NewLogStatusNotificationRequest(status, requestID) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*diagnostics.LogStatusNotificationResponse), err + } +} + +func (cs *chargingStation) MeterValues(evseID int, meterValues []types.MeterValue, props ...func(request *meter.MeterValuesRequest)) (*meter.MeterValuesResponse, error) { + request := meter.NewMeterValuesRequest(evseID, meterValues) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*meter.MeterValuesResponse), err + } +} + +func (cs *chargingStation) NotifyChargingLimit(chargingLimit smartcharging.ChargingLimit, props ...func(request *smartcharging.NotifyChargingLimitRequest)) (*smartcharging.NotifyChargingLimitResponse, error) { + request := smartcharging.NewNotifyChargingLimitRequest(chargingLimit) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*smartcharging.NotifyChargingLimitResponse), err + } +} + +func (cs *chargingStation) NotifyCustomerInformation(data string, seqNo int, generatedAt types.DateTime, requestID int, props ...func(request *diagnostics.NotifyCustomerInformationRequest)) (*diagnostics.NotifyCustomerInformationResponse, error) { + request := diagnostics.NewNotifyCustomerInformationRequest(data, seqNo, generatedAt, requestID) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*diagnostics.NotifyCustomerInformationResponse), err + } +} + +func (cs *chargingStation) NotifyDisplayMessages(requestID int, props ...func(request *display.NotifyDisplayMessagesRequest)) (*display.NotifyDisplayMessagesResponse, error) { + request := display.NewNotifyDisplayMessagesRequest(requestID) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*display.NotifyDisplayMessagesResponse), err + } +} + +func (cs *chargingStation) NotifyEVChargingNeeds(evseID int, chargingNeeds smartcharging.ChargingNeeds, props ...func(request *smartcharging.NotifyEVChargingNeedsRequest)) (*smartcharging.NotifyEVChargingNeedsResponse, error) { + request := smartcharging.NewNotifyEVChargingNeedsRequest(evseID, chargingNeeds) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*smartcharging.NotifyEVChargingNeedsResponse), err + } +} + +func (cs *chargingStation) NotifyEVChargingSchedule(timeBase *types.DateTime, evseID int, schedule types.ChargingSchedule, props ...func(request *smartcharging.NotifyEVChargingScheduleRequest)) (*smartcharging.NotifyEVChargingScheduleResponse, error) { + request := smartcharging.NewNotifyEVChargingScheduleRequest(timeBase, evseID, schedule) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*smartcharging.NotifyEVChargingScheduleResponse), err + } +} + +func (cs *chargingStation) NotifyEvent(generatedAt *types.DateTime, seqNo int, eventData []diagnostics.EventData, props ...func(request *diagnostics.NotifyEventRequest)) (*diagnostics.NotifyEventResponse, error) { + request := diagnostics.NewNotifyEventRequest(generatedAt, seqNo, eventData) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*diagnostics.NotifyEventResponse), err + } +} + +func (cs *chargingStation) NotifyMonitoringReport(requestID int, seqNo int, generatedAt *types.DateTime, monitorData []diagnostics.MonitoringData, props ...func(request *diagnostics.NotifyMonitoringReportRequest)) (*diagnostics.NotifyMonitoringReportResponse, error) { + request := diagnostics.NewNotifyMonitoringReportRequest(requestID, seqNo, generatedAt, monitorData) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*diagnostics.NotifyMonitoringReportResponse), err + } +} + +func (cs *chargingStation) NotifyReport(requestID int, generatedAt *types.DateTime, seqNo int, props ...func(request *provisioning.NotifyReportRequest)) (*provisioning.NotifyReportResponse, error) { + request := provisioning.NewNotifyReportRequest(requestID, generatedAt, seqNo) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*provisioning.NotifyReportResponse), err + } +} + +func (cs *chargingStation) PublishFirmwareStatusNotification(status firmware.PublishFirmwareStatus, props ...func(request *firmware.PublishFirmwareStatusNotificationRequest)) (*firmware.PublishFirmwareStatusNotificationResponse, error) { + request := firmware.NewPublishFirmwareStatusNotificationRequest(status) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*firmware.PublishFirmwareStatusNotificationResponse), err + } +} + +func (cs *chargingStation) ReportChargingProfiles(requestID int, chargingLimitSource types.ChargingLimitSourceType, evseID int, chargingProfile []types.ChargingProfile, props ...func(request *smartcharging.ReportChargingProfilesRequest)) (*smartcharging.ReportChargingProfilesResponse, error) { + request := smartcharging.NewReportChargingProfilesRequest(requestID, chargingLimitSource, evseID, chargingProfile) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*smartcharging.ReportChargingProfilesResponse), err + } +} + +func (cs *chargingStation) ReservationStatusUpdate(reservationID int, status reservation.ReservationUpdateStatus, props ...func(request *reservation.ReservationStatusUpdateRequest)) (*reservation.ReservationStatusUpdateResponse, error) { + request := reservation.NewReservationStatusUpdateRequest(reservationID, status) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*reservation.ReservationStatusUpdateResponse), err + } +} + +func (cs *chargingStation) SecurityEventNotification(typ string, timestamp *types.DateTime, props ...func(request *security.SecurityEventNotificationRequest)) (*security.SecurityEventNotificationResponse, error) { + request := security.NewSecurityEventNotificationRequest(typ, timestamp) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*security.SecurityEventNotificationResponse), err + } +} + +func (cs *chargingStation) SignCertificate(csr string, props ...func(request *security.SignCertificateRequest)) (*security.SignCertificateResponse, error) { + request := security.NewSignCertificateRequest(csr) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*security.SignCertificateResponse), err + } +} + +func (cs *chargingStation) StatusNotification(timestamp *types.DateTime, status availability.ConnectorStatus, evseID int, connectorID int, props ...func(request *availability.StatusNotificationRequest)) (*availability.StatusNotificationResponse, error) { + request := availability.NewStatusNotificationRequest(timestamp, status, evseID, connectorID) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*availability.StatusNotificationResponse), err + } +} + +func (cs *chargingStation) TransactionEvent(t transactions.TransactionEvent, timestamp *types.DateTime, reason transactions.TriggerReason, seqNo int, info transactions.Transaction, props ...func(request *transactions.TransactionEventRequest)) (*transactions.TransactionEventResponse, error) { + request := transactions.NewTransactionEventRequest(t, timestamp, reason, seqNo, info) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } else { + return response.(*transactions.TransactionEventResponse), err + } +} + +func (cs *chargingStation) SetSecurityHandler(handler security.ChargingStationHandler) { + cs.securityHandler = handler +} + +func (cs *chargingStation) SetProvisioningHandler(handler provisioning.ChargingStationHandler) { + cs.provisioningHandler = handler +} + +func (cs *chargingStation) SetAuthorizationHandler(handler authorization.ChargingStationHandler) { + cs.authorizationHandler = handler +} + +func (cs *chargingStation) SetLocalAuthListHandler(handler localauth.ChargingStationHandler) { + cs.localAuthListHandler = handler +} + +func (cs *chargingStation) SetTransactionsHandler(handler transactions.ChargingStationHandler) { + cs.transactionsHandler = handler +} + +func (cs *chargingStation) SetRemoteControlHandler(handler remotecontrol.ChargingStationHandler) { + cs.remoteControlHandler = handler +} + +func (cs *chargingStation) SetAvailabilityHandler(handler availability.ChargingStationHandler) { + cs.availabilityHandler = handler +} + +func (cs *chargingStation) SetReservationHandler(handler reservation.ChargingStationHandler) { + cs.reservationHandler = handler +} + +func (cs *chargingStation) SetTariffCostHandler(handler tariffcost.ChargingStationHandler) { + cs.tariffCostHandler = handler +} + +func (cs *chargingStation) SetMeterHandler(handler meter.ChargingStationHandler) { + cs.meterHandler = handler +} + +func (cs *chargingStation) SetSmartChargingHandler(handler smartcharging.ChargingStationHandler) { + cs.smartChargingHandler = handler +} + +func (cs *chargingStation) SetFirmwareHandler(handler firmware.ChargingStationHandler) { + cs.firmwareHandler = handler +} + +func (cs *chargingStation) SetISO15118Handler(handler iso15118.ChargingStationHandler) { + cs.iso15118Handler = handler +} + +func (cs *chargingStation) SetDiagnosticsHandler(handler diagnostics.ChargingStationHandler) { + cs.diagnosticsHandler = handler +} + +func (cs *chargingStation) SetDisplayHandler(handler display.ChargingStationHandler) { + cs.displayHandler = handler +} + +func (cs *chargingStation) SetDataHandler(handler data.ChargingStationHandler) { + cs.dataHandler = handler +} + +func (cs *chargingStation) SendRequest(request ocpp.Request) (ocpp.Response, error) { + featureName := request.GetFeatureName() + if _, found := cs.client.GetProfileForFeature(featureName); !found { + return nil, fmt.Errorf("feature %v is unsupported on charging station (missing profile), cannot send request", featureName) + } + + // Wraps an asynchronous response + type asyncResponse struct { + r ocpp.Response + e error + } + // Create channel and pass it to a callback function, for retrieving asynchronous response + asyncResponseC := make(chan asyncResponse, 1) + send := func() error { + return cs.client.SendRequest(request) + } + err := cs.callbacks.TryQueue("main", send, func(confirmation ocpp.Response, err error) { + asyncResponseC <- asyncResponse{r: confirmation, e: err} + }) + if err != nil { + return nil, err + } + asyncResult, ok := <-asyncResponseC + if !ok { + return nil, fmt.Errorf("internal error while receiving result for %v request", request.GetFeatureName()) + } + return asyncResult.r, asyncResult.e +} + +func (cs *chargingStation) SendRequestAsync(request ocpp.Request, callback func(response ocpp.Response, err error)) error { + featureName := request.GetFeatureName() + if _, found := cs.client.GetProfileForFeature(featureName); !found { + return fmt.Errorf("feature %v is unsupported on charging station (missing profile), cannot send request", featureName) + } + switch featureName { + case authorization.AuthorizeFeatureName, + provisioning.BootNotificationFeatureName, + smartcharging.ClearedChargingLimitFeatureName, + data.DataTransferFeatureName, + firmware.FirmwareStatusNotificationFeatureName, + iso15118.Get15118EVCertificateFeatureName, + iso15118.GetCertificateStatusFeatureName, + availability.HeartbeatFeatureName, + diagnostics.LogStatusNotificationFeatureName, + meter.MeterValuesFeatureName, + smartcharging.NotifyChargingLimitFeatureName, + diagnostics.NotifyCustomerInformationFeatureName, + display.NotifyDisplayMessagesFeatureName, + smartcharging.NotifyEVChargingNeedsFeatureName, + smartcharging.NotifyEVChargingScheduleFeatureName, + diagnostics.NotifyEventFeatureName, + diagnostics.NotifyMonitoringReportFeatureName, + provisioning.NotifyReportFeatureName, + firmware.PublishFirmwareStatusNotificationFeatureName, + smartcharging.ReportChargingProfilesFeatureName, + reservation.ReservationStatusUpdateFeatureName, + security.SecurityEventNotificationFeatureName, + security.SignCertificateFeatureName, + availability.StatusNotificationFeatureName, + transactions.TransactionEventFeatureName: + break + default: + return fmt.Errorf("unsupported action %v on charging station, cannot send request", featureName) + } + // Response will be retrieved asynchronously via asyncHandler + send := func() error { + return cs.client.SendRequest(request) + } + err := cs.callbacks.TryQueue("main", send, callback) + return err +} + +func (cs *chargingStation) asyncCallbackHandler() { + for { + select { + case confirmation := <-cs.responseHandler: + // Get and invoke callback + if callback, ok := cs.callbacks.Dequeue("main"); ok { + callback(confirmation, nil) + } else { + cs.error(fmt.Errorf("no callback available for incoming response %v", confirmation.GetFeatureName())) + } + case protoError := <-cs.errorHandler: + // Get and invoke callback + if callback, ok := cs.callbacks.Dequeue("main"); ok { + callback(nil, protoError) + } else { + cs.error(fmt.Errorf("no callback available for incoming error %w", protoError)) + } + case <-cs.stopC: + return + } + } +} + +func (cs *chargingStation) sendResponse(response ocpp.Response, err error, requestId string) { + if err != nil { + // Send error response + if ocppError, ok := err.(*ocpp.Error); ok { + err = cs.client.SendError(requestId, ocppError.Code, ocppError.Description, nil) + } else { + err = cs.client.SendError(requestId, ocppj.InternalError, err.Error(), nil) + } + if err != nil { + // 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 || 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 confirmation response + err = cs.client.SendResponse(requestId, response) + if err != nil { + // 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 { + // Start client + cs.stopC = make(chan struct{}, 1) + err := cs.client.Start(csmsUrl) + // Async response handler receives incoming responses/errors and triggers callbacks + if err == nil { + go cs.asyncCallbackHandler() + } + return err +} + +func (cs *chargingStation) StartWithRetries(csmsUrl string) { + // Start client + cs.stopC = make(chan struct{}, 1) + cs.client.StartWithRetries(csmsUrl) + // Async response handler receives incoming responses/errors and triggers callbacks + go cs.asyncCallbackHandler() +} + +func (cs *chargingStation) Stop() { + cs.client.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 { + cs.error(fmt.Errorf("replying csms to request %v with error: %w", requestId, err)) + } +} + +func (cs *chargingStation) notSupportedError(requestId string, action string) { + err := cs.client.SendError(requestId, ocppj.NotSupported, fmt.Sprintf("unsupported action %v on charging station", action), nil) + if err != nil { + cs.error(fmt.Errorf("replying csms to request %s with 'not supported': %w", requestId, err)) + } +} + +func (cs *chargingStation) handleIncomingRequest(request ocpp.Request, requestId string, action string) { + profile, found := cs.client.GetProfileForFeature(action) + // Check whether action is supported and a listener for it exists + if !found { + cs.notImplementedError(requestId, action) + return + } else { + supported := true + switch profile.Name { + case authorization.ProfileName: + if cs.authorizationHandler == nil { + supported = false + } + case availability.ProfileName: + if cs.availabilityHandler == nil { + supported = false + } + case data.ProfileName: + if cs.dataHandler == nil { + supported = false + } + case diagnostics.ProfileName: + if cs.diagnosticsHandler == nil { + supported = false + } + case display.ProfileName: + if cs.displayHandler == nil { + supported = false + } + case firmware.ProfileName: + if cs.firmwareHandler == nil { + supported = false + } + case iso15118.ProfileName: + if cs.iso15118Handler == nil { + supported = false + } + case localauth.ProfileName: + if cs.localAuthListHandler == nil { + supported = false + } + case meter.ProfileName: + if cs.meterHandler == nil { + supported = false + } + case provisioning.ProfileName: + if cs.provisioningHandler == nil { + supported = false + } + case remotecontrol.ProfileName: + if cs.remoteControlHandler == nil { + supported = false + } + case reservation.ProfileName: + if cs.reservationHandler == nil { + supported = false + } + case security.ProfileName: + if cs.securityHandler == nil { + supported = false + } + case smartcharging.ProfileName: + if cs.smartChargingHandler == nil { + supported = false + } + case tariffcost.ProfileName: + if cs.tariffCostHandler == nil { + supported = false + } + case transactions.ProfileName: + if cs.transactionsHandler == nil { + supported = false + } + } + if !supported { + cs.notSupportedError(requestId, action) + return + } + } + // Process request + var response ocpp.Response + var err error + switch action { + case reservation.CancelReservationFeatureName: + response, err = cs.reservationHandler.OnCancelReservation(request.(*reservation.CancelReservationRequest)) + case security.CertificateSignedFeatureName: + response, err = cs.securityHandler.OnCertificateSigned(request.(*security.CertificateSignedRequest)) + case availability.ChangeAvailabilityFeatureName: + response, err = cs.availabilityHandler.OnChangeAvailability(request.(*availability.ChangeAvailabilityRequest)) + case authorization.ClearCacheFeatureName: + response, err = cs.authorizationHandler.OnClearCache(request.(*authorization.ClearCacheRequest)) + case smartcharging.ClearChargingProfileFeatureName: + response, err = cs.smartChargingHandler.OnClearChargingProfile(request.(*smartcharging.ClearChargingProfileRequest)) + case display.ClearDisplayMessageFeatureName: + response, err = cs.displayHandler.OnClearDisplay(request.(*display.ClearDisplayRequest)) + case diagnostics.ClearVariableMonitoringFeatureName: + response, err = cs.diagnosticsHandler.OnClearVariableMonitoring(request.(*diagnostics.ClearVariableMonitoringRequest)) + case tariffcost.CostUpdatedFeatureName: + response, err = cs.tariffCostHandler.OnCostUpdated(request.(*tariffcost.CostUpdatedRequest)) + case diagnostics.CustomerInformationFeatureName: + response, err = cs.diagnosticsHandler.OnCustomerInformation(request.(*diagnostics.CustomerInformationRequest)) + case data.DataTransferFeatureName: + response, err = cs.dataHandler.OnDataTransfer(request.(*data.DataTransferRequest)) + case iso15118.DeleteCertificateFeatureName: + response, err = cs.iso15118Handler.OnDeleteCertificate(request.(*iso15118.DeleteCertificateRequest)) + case provisioning.GetBaseReportFeatureName: + response, err = cs.provisioningHandler.OnGetBaseReport(request.(*provisioning.GetBaseReportRequest)) + case smartcharging.GetChargingProfilesFeatureName: + response, err = cs.smartChargingHandler.OnGetChargingProfiles(request.(*smartcharging.GetChargingProfilesRequest)) + case smartcharging.GetCompositeScheduleFeatureName: + response, err = cs.smartChargingHandler.OnGetCompositeSchedule(request.(*smartcharging.GetCompositeScheduleRequest)) + case display.GetDisplayMessagesFeatureName: + response, err = cs.displayHandler.OnGetDisplayMessages(request.(*display.GetDisplayMessagesRequest)) + case iso15118.GetInstalledCertificateIdsFeatureName: + response, err = cs.iso15118Handler.OnGetInstalledCertificateIds(request.(*iso15118.GetInstalledCertificateIdsRequest)) + case localauth.GetLocalListVersionFeatureName: + response, err = cs.localAuthListHandler.OnGetLocalListVersion(request.(*localauth.GetLocalListVersionRequest)) + case diagnostics.GetLogFeatureName: + response, err = cs.diagnosticsHandler.OnGetLog(request.(*diagnostics.GetLogRequest)) + case diagnostics.GetMonitoringReportFeatureName: + response, err = cs.diagnosticsHandler.OnGetMonitoringReport(request.(*diagnostics.GetMonitoringReportRequest)) + case provisioning.GetReportFeatureName: + response, err = cs.provisioningHandler.OnGetReport(request.(*provisioning.GetReportRequest)) + case transactions.GetTransactionStatusFeatureName: + response, err = cs.transactionsHandler.OnGetTransactionStatus(request.(*transactions.GetTransactionStatusRequest)) + case provisioning.GetVariablesFeatureName: + response, err = cs.provisioningHandler.OnGetVariables(request.(*provisioning.GetVariablesRequest)) + case iso15118.InstallCertificateFeatureName: + response, err = cs.iso15118Handler.OnInstallCertificate(request.(*iso15118.InstallCertificateRequest)) + case firmware.PublishFirmwareFeatureName: + response, err = cs.firmwareHandler.OnPublishFirmware(request.(*firmware.PublishFirmwareRequest)) + case remotecontrol.RequestStartTransactionFeatureName: + response, err = cs.remoteControlHandler.OnRequestStartTransaction(request.(*remotecontrol.RequestStartTransactionRequest)) + case remotecontrol.RequestStopTransactionFeatureName: + response, err = cs.remoteControlHandler.OnRequestStopTransaction(request.(*remotecontrol.RequestStopTransactionRequest)) + case reservation.ReserveNowFeatureName: + response, err = cs.reservationHandler.OnReserveNow(request.(*reservation.ReserveNowRequest)) + case provisioning.ResetFeatureName: + response, err = cs.provisioningHandler.OnReset(request.(*provisioning.ResetRequest)) + case localauth.SendLocalListFeatureName: + response, err = cs.localAuthListHandler.OnSendLocalList(request.(*localauth.SendLocalListRequest)) + case smartcharging.SetChargingProfileFeatureName: + response, err = cs.smartChargingHandler.OnSetChargingProfile(request.(*smartcharging.SetChargingProfileRequest)) + case display.SetDisplayMessageFeatureName: + response, err = cs.displayHandler.OnSetDisplayMessage(request.(*display.SetDisplayMessageRequest)) + case diagnostics.SetMonitoringBaseFeatureName: + response, err = cs.diagnosticsHandler.OnSetMonitoringBase(request.(*diagnostics.SetMonitoringBaseRequest)) + case diagnostics.SetMonitoringLevelFeatureName: + response, err = cs.diagnosticsHandler.OnSetMonitoringLevel(request.(*diagnostics.SetMonitoringLevelRequest)) + case provisioning.SetNetworkProfileFeatureName: + response, err = cs.provisioningHandler.OnSetNetworkProfile(request.(*provisioning.SetNetworkProfileRequest)) + case diagnostics.SetVariableMonitoringFeatureName: + response, err = cs.diagnosticsHandler.OnSetVariableMonitoring(request.(*diagnostics.SetVariableMonitoringRequest)) + case provisioning.SetVariablesFeatureName: + response, err = cs.provisioningHandler.OnSetVariables(request.(*provisioning.SetVariablesRequest)) + case remotecontrol.TriggerMessageFeatureName: + response, err = cs.remoteControlHandler.OnTriggerMessage(request.(*remotecontrol.TriggerMessageRequest)) + case remotecontrol.UnlockConnectorFeatureName: + response, err = cs.remoteControlHandler.OnUnlockConnector(request.(*remotecontrol.UnlockConnectorRequest)) + case firmware.UnpublishFirmwareFeatureName: + response, err = cs.firmwareHandler.OnUnpublishFirmware(request.(*firmware.UnpublishFirmwareRequest)) + case firmware.UpdateFirmwareFeatureName: + response, err = cs.firmwareHandler.OnUpdateFirmware(request.(*firmware.UpdateFirmwareRequest)) + default: + cs.notSupportedError(requestId, action) + return + } + cs.sendResponse(response, err, requestId) +} diff --git a/ocpp2.1/csms.go b/ocpp2.1/csms.go new file mode 100644 index 00000000..914f4a6f --- /dev/null +++ b/ocpp2.1/csms.go @@ -0,0 +1,1049 @@ +package ocpp21 + +import ( + "fmt" + "reflect" + + "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/authorization" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/data" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/diagnostics" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/display" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/iso15118" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/meter" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/remotecontrol" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/security" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/tariffcost" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/transactions" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/lorenzodonini/ocpp-go/ws" +) + +type csms struct { + server *ocppj.Server + securityHandler security.CSMSHandler + provisioningHandler provisioning.CSMSHandler + authorizationHandler authorization.CSMSHandler + localAuthListHandler localauth.CSMSHandler + transactionsHandler transactions.CSMSHandler + remoteControlHandler remotecontrol.CSMSHandler + availabilityHandler availability.CSMSHandler + reservationHandler reservation.CSMSHandler + tariffCostHandler tariffcost.CSMSHandler + meterHandler meter.CSMSHandler + smartChargingHandler smartcharging.CSMSHandler + firmwareHandler firmware.CSMSHandler + iso15118Handler iso15118.CSMSHandler + diagnosticsHandler diagnostics.CSMSHandler + displayHandler display.CSMSHandler + dataHandler data.CSMSHandler + callbackQueue callbackqueue.CallbackQueue + errC chan error +} + +func newCSMS(server *ocppj.Server) csms { + if server == nil { + panic("server must not be nil") + } + server.SetDialect(ocpp.V2) + return csms{ + server: server, + callbackQueue: callbackqueue.New(), + } +} + +func (cs *csms) error(err error) { + if cs.errC != nil { + cs.errC <- err + } +} + +func (cs *csms) Errors() <-chan error { + if cs.errC == nil { + cs.errC = make(chan error, 1) + } + return cs.errC +} + +func (cs *csms) CancelReservation(clientId string, callback func(*reservation.CancelReservationResponse, error), reservationId int, props ...func(request *reservation.CancelReservationRequest)) error { + request := reservation.NewCancelReservationRequest(reservationId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*reservation.CancelReservationResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) CertificateSigned(clientId string, callback func(*security.CertificateSignedResponse, error), certificateChain string, props ...func(*security.CertificateSignedRequest)) error { + request := security.NewCertificateSignedRequest(certificateChain) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*security.CertificateSignedResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ChangeAvailability(clientId string, callback func(*availability.ChangeAvailabilityResponse, error), operationalStatus availability.OperationalStatus, props ...func(request *availability.ChangeAvailabilityRequest)) error { + request := availability.NewChangeAvailabilityRequest(operationalStatus) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*availability.ChangeAvailabilityResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ClearCache(clientId string, callback func(*authorization.ClearCacheResponse, error), props ...func(*authorization.ClearCacheRequest)) error { + request := authorization.NewClearCacheRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*authorization.ClearCacheResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ClearChargingProfile(clientId string, callback func(*smartcharging.ClearChargingProfileResponse, error), props ...func(request *smartcharging.ClearChargingProfileRequest)) error { + request := smartcharging.NewClearChargingProfileRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*smartcharging.ClearChargingProfileResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ClearDisplay(clientId string, callback func(*display.ClearDisplayResponse, error), id int, props ...func(*display.ClearDisplayRequest)) error { + request := display.NewClearDisplayRequest(id) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*display.ClearDisplayResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ClearVariableMonitoring(clientId string, callback func(*diagnostics.ClearVariableMonitoringResponse, error), id []int, props ...func(*diagnostics.ClearVariableMonitoringRequest)) error { + request := diagnostics.NewClearVariableMonitoringRequest(id) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.ClearVariableMonitoringResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) CostUpdated(clientId string, callback func(*tariffcost.CostUpdatedResponse, error), totalCost float64, transactionId string, props ...func(*tariffcost.CostUpdatedRequest)) error { + request := tariffcost.NewCostUpdatedRequest(totalCost, transactionId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*tariffcost.CostUpdatedResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) CustomerInformation(clientId string, callback func(*diagnostics.CustomerInformationResponse, error), requestId int, report bool, clear bool, props ...func(*diagnostics.CustomerInformationRequest)) error { + request := diagnostics.NewCustomerInformationRequest(requestId, report, clear) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.CustomerInformationResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) DataTransfer(clientId string, callback func(*data.DataTransferResponse, error), vendorId string, props ...func(request *data.DataTransferRequest)) error { + request := data.NewDataTransferRequest(vendorId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*data.DataTransferResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) DeleteCertificate(clientId string, callback func(*iso15118.DeleteCertificateResponse, error), data types.CertificateHashData, props ...func(*iso15118.DeleteCertificateRequest)) error { + request := iso15118.NewDeleteCertificateRequest(data) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*iso15118.DeleteCertificateResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetBaseReport(clientId string, callback func(*provisioning.GetBaseReportResponse, error), requestId int, reportBase provisioning.ReportBaseType, props ...func(*provisioning.GetBaseReportRequest)) error { + request := provisioning.NewGetBaseReportRequest(requestId, reportBase) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*provisioning.GetBaseReportResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetChargingProfiles(clientId string, callback func(*smartcharging.GetChargingProfilesResponse, error), chargingProfile smartcharging.ChargingProfileCriterion, props ...func(*smartcharging.GetChargingProfilesRequest)) error { + request := smartcharging.NewGetChargingProfilesRequest(chargingProfile) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*smartcharging.GetChargingProfilesResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetCompositeSchedule(clientId string, callback func(*smartcharging.GetCompositeScheduleResponse, error), duration int, evseId int, props ...func(*smartcharging.GetCompositeScheduleRequest)) error { + request := smartcharging.NewGetCompositeScheduleRequest(duration, evseId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*smartcharging.GetCompositeScheduleResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetDisplayMessages(clientId string, callback func(*display.GetDisplayMessagesResponse, error), requestId int, props ...func(*display.GetDisplayMessagesRequest)) error { + request := display.NewGetDisplayMessagesRequest(requestId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*display.GetDisplayMessagesResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +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) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*iso15118.GetInstalledCertificateIdsResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetLocalListVersion(clientId string, callback func(*localauth.GetLocalListVersionResponse, error), props ...func(*localauth.GetLocalListVersionRequest)) error { + request := localauth.NewGetLocalListVersionRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*localauth.GetLocalListVersionResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetLog(clientId string, callback func(*diagnostics.GetLogResponse, error), logType diagnostics.LogType, requestID int, logParameters diagnostics.LogParameters, props ...func(*diagnostics.GetLogRequest)) error { + request := diagnostics.NewGetLogRequest(logType, requestID, logParameters) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.GetLogResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetMonitoringReport(clientId string, callback func(*diagnostics.GetMonitoringReportResponse, error), props ...func(*diagnostics.GetMonitoringReportRequest)) error { + request := diagnostics.NewGetMonitoringReportRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.GetMonitoringReportResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetReport(clientId string, callback func(*provisioning.GetReportResponse, error), props ...func(*provisioning.GetReportRequest)) error { + request := provisioning.NewGetReportRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*provisioning.GetReportResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetTransactionStatus(clientId string, callback func(*transactions.GetTransactionStatusResponse, error), props ...func(*transactions.GetTransactionStatusRequest)) error { + request := transactions.NewGetTransactionStatusRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*transactions.GetTransactionStatusResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetVariables(clientId string, callback func(*provisioning.GetVariablesResponse, error), variableData []provisioning.GetVariableData, props ...func(*provisioning.GetVariablesRequest)) error { + request := provisioning.NewGetVariablesRequest(variableData) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*provisioning.GetVariablesResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) InstallCertificate(clientId string, callback func(*iso15118.InstallCertificateResponse, error), certificateType types.CertificateUse, certificate string, props ...func(*iso15118.InstallCertificateRequest)) error { + request := iso15118.NewInstallCertificateRequest(certificateType, certificate) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*iso15118.InstallCertificateResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) PublishFirmware(clientId string, callback func(*firmware.PublishFirmwareResponse, error), location string, checksum string, requestID int, props ...func(request *firmware.PublishFirmwareRequest)) error { + request := firmware.NewPublishFirmwareRequest(location, checksum, requestID) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*firmware.PublishFirmwareResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +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) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*remotecontrol.RequestStartTransactionResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) RequestStopTransaction(clientId string, callback func(*remotecontrol.RequestStopTransactionResponse, error), transactionID string, props ...func(request *remotecontrol.RequestStopTransactionRequest)) error { + request := remotecontrol.NewRequestStopTransactionRequest(transactionID) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*remotecontrol.RequestStopTransactionResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +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) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*reservation.ReserveNowResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) Reset(clientId string, callback func(*provisioning.ResetResponse, error), t provisioning.ResetType, props ...func(request *provisioning.ResetRequest)) error { + request := provisioning.NewResetRequest(t) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*provisioning.ResetResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SendLocalList(clientId string, callback func(*localauth.SendLocalListResponse, error), version int, updateType localauth.UpdateType, props ...func(request *localauth.SendLocalListRequest)) error { + request := localauth.NewSendLocalListRequest(version, updateType) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*localauth.SendLocalListResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetChargingProfile(clientId string, callback func(*smartcharging.SetChargingProfileResponse, error), evseID int, chargingProfile *types.ChargingProfile, props ...func(request *smartcharging.SetChargingProfileRequest)) error { + request := smartcharging.NewSetChargingProfileRequest(evseID, chargingProfile) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*smartcharging.SetChargingProfileResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetDisplayMessage(clientId string, callback func(*display.SetDisplayMessageResponse, error), message display.MessageInfo, props ...func(request *display.SetDisplayMessageRequest)) error { + request := display.NewSetDisplayMessageRequest(message) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*display.SetDisplayMessageResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetMonitoringBase(clientId string, callback func(*diagnostics.SetMonitoringBaseResponse, error), monitoringBase diagnostics.MonitoringBase, props ...func(request *diagnostics.SetMonitoringBaseRequest)) error { + request := diagnostics.NewSetMonitoringBaseRequest(monitoringBase) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.SetMonitoringBaseResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetMonitoringLevel(clientId string, callback func(*diagnostics.SetMonitoringLevelResponse, error), severity int, props ...func(request *diagnostics.SetMonitoringLevelRequest)) error { + request := diagnostics.NewSetMonitoringLevelRequest(severity) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.SetMonitoringLevelResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetNetworkProfile(clientId string, callback func(*provisioning.SetNetworkProfileResponse, error), configurationSlot int, connectionData provisioning.NetworkConnectionProfile, props ...func(request *provisioning.SetNetworkProfileRequest)) error { + request := provisioning.NewSetNetworkProfileRequest(configurationSlot, connectionData) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*provisioning.SetNetworkProfileResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetVariableMonitoring(clientId string, callback func(*diagnostics.SetVariableMonitoringResponse, error), data []diagnostics.SetMonitoringData, props ...func(request *diagnostics.SetVariableMonitoringRequest)) error { + request := diagnostics.NewSetVariableMonitoringRequest(data) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.SetVariableMonitoringResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetVariables(clientId string, callback func(*provisioning.SetVariablesResponse, error), data []provisioning.SetVariableData, props ...func(request *provisioning.SetVariablesRequest)) error { + request := provisioning.NewSetVariablesRequest(data) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*provisioning.SetVariablesResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) TriggerMessage(clientId string, callback func(*remotecontrol.TriggerMessageResponse, error), requestedMessage remotecontrol.MessageTrigger, props ...func(request *remotecontrol.TriggerMessageRequest)) error { + request := remotecontrol.NewTriggerMessageRequest(requestedMessage) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*remotecontrol.TriggerMessageResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) UnlockConnector(clientId string, callback func(*remotecontrol.UnlockConnectorResponse, error), evseID int, connectorID int, props ...func(request *remotecontrol.UnlockConnectorRequest)) error { + request := remotecontrol.NewUnlockConnectorRequest(evseID, connectorID) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*remotecontrol.UnlockConnectorResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) UnpublishFirmware(clientId string, callback func(*firmware.UnpublishFirmwareResponse, error), checksum string, props ...func(request *firmware.UnpublishFirmwareRequest)) error { + request := firmware.NewUnpublishFirmwareRequest(checksum) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*firmware.UnpublishFirmwareResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) UpdateFirmware(clientId string, callback func(*firmware.UpdateFirmwareResponse, error), requestID int, f firmware.Firmware, props ...func(request *firmware.UpdateFirmwareRequest)) error { + request := firmware.NewUpdateFirmwareRequest(requestID, f) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*firmware.UpdateFirmwareResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetSecurityHandler(handler security.CSMSHandler) { + cs.securityHandler = handler +} + +func (cs *csms) SetProvisioningHandler(handler provisioning.CSMSHandler) { + cs.provisioningHandler = handler +} + +func (cs *csms) SetAuthorizationHandler(handler authorization.CSMSHandler) { + cs.authorizationHandler = handler +} + +func (cs *csms) SetLocalAuthListHandler(handler localauth.CSMSHandler) { + cs.localAuthListHandler = handler +} + +func (cs *csms) SetTransactionsHandler(handler transactions.CSMSHandler) { + cs.transactionsHandler = handler +} + +func (cs *csms) SetRemoteControlHandler(handler remotecontrol.CSMSHandler) { + cs.remoteControlHandler = handler +} + +func (cs *csms) SetAvailabilityHandler(handler availability.CSMSHandler) { + cs.availabilityHandler = handler +} + +func (cs *csms) SetReservationHandler(handler reservation.CSMSHandler) { + cs.reservationHandler = handler +} + +func (cs *csms) SetTariffCostHandler(handler tariffcost.CSMSHandler) { + cs.tariffCostHandler = handler +} + +func (cs *csms) SetMeterHandler(handler meter.CSMSHandler) { + cs.meterHandler = handler +} + +func (cs *csms) SetSmartChargingHandler(handler smartcharging.CSMSHandler) { + cs.smartChargingHandler = handler +} + +func (cs *csms) SetFirmwareHandler(handler firmware.CSMSHandler) { + cs.firmwareHandler = handler +} + +func (cs *csms) SetISO15118Handler(handler iso15118.CSMSHandler) { + cs.iso15118Handler = handler +} + +func (cs *csms) SetDiagnosticsHandler(handler diagnostics.CSMSHandler) { + cs.diagnosticsHandler = handler +} + +func (cs *csms) SetDisplayHandler(handler display.CSMSHandler) { + cs.displayHandler = handler +} + +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) + }) +} + +func (cs *csms) SetChargingStationDisconnectedHandler(handler ChargingStationConnectionHandler) { + cs.server.SetDisconnectedClientHandler(func(chargingStation ws.Channel) { + for cb, ok := cs.callbackQueue.Dequeue(chargingStation.ID()); ok; cb, ok = cs.callbackQueue.Dequeue(chargingStation.ID()) { + err := ocpp.NewError(ocppj.GenericError, "client disconnected, no response received from client", "") + cb(nil, err) + } + handler(chargingStation) + }) +} + +func (cs *csms) SendRequestAsync(clientId string, request ocpp.Request, callback func(response ocpp.Response, err error)) error { + featureName := request.GetFeatureName() + if _, found := cs.server.GetProfileForFeature(featureName); !found { + return fmt.Errorf("feature %v is unsupported on CSMS (missing profile), cannot send request", featureName) + } + switch featureName { + case reservation.CancelReservationFeatureName, + security.CertificateSignedFeatureName, + availability.ChangeAvailabilityFeatureName, + authorization.ClearCacheFeatureName, + smartcharging.ClearChargingProfileFeatureName, + display.ClearDisplayMessageFeatureName, + diagnostics.ClearVariableMonitoringFeatureName, + tariffcost.CostUpdatedFeatureName, + diagnostics.CustomerInformationFeatureName, + data.DataTransferFeatureName, + iso15118.DeleteCertificateFeatureName, + provisioning.GetBaseReportFeatureName, + smartcharging.GetChargingProfilesFeatureName, + smartcharging.GetCompositeScheduleFeatureName, + display.GetDisplayMessagesFeatureName, + iso15118.GetInstalledCertificateIdsFeatureName, + localauth.GetLocalListVersionFeatureName, + diagnostics.GetLogFeatureName, + diagnostics.GetMonitoringReportFeatureName, + provisioning.GetReportFeatureName, + transactions.GetTransactionStatusFeatureName, + provisioning.GetVariablesFeatureName, + iso15118.InstallCertificateFeatureName, + firmware.PublishFirmwareFeatureName, + remotecontrol.RequestStartTransactionFeatureName, + remotecontrol.RequestStopTransactionFeatureName, + reservation.ReserveNowFeatureName, + provisioning.ResetFeatureName, + localauth.SendLocalListFeatureName, + smartcharging.SetChargingProfileFeatureName, + display.SetDisplayMessageFeatureName, + diagnostics.SetMonitoringBaseFeatureName, + diagnostics.SetMonitoringLevelFeatureName, + provisioning.SetNetworkProfileFeatureName, + diagnostics.SetVariableMonitoringFeatureName, + provisioning.SetVariablesFeatureName, + remotecontrol.TriggerMessageFeatureName, + remotecontrol.UnlockConnectorFeatureName, + firmware.UnpublishFirmwareFeatureName, + firmware.UpdateFirmwareFeatureName: + break + default: + return fmt.Errorf("unsupported action %v on CSMS, cannot send request", featureName) + } + + send := func() error { + return cs.server.SendRequest(clientId, request) + } + return cs.callbackQueue.TryQueue(clientId, send, callback) +} + +func (cs *csms) Start(listenPort int, listenPath string) { + // Start server + cs.server.Start(listenPort, listenPath) +} + +func (cs *csms) Stop() { + cs.server.Stop() +} + +func (cs *csms) sendResponse(chargingStationID string, response ocpp.Response, err error, requestId string) { + if err != nil { + // Send error response + if ocppError, ok := err.(*ocpp.Error); ok { + err = cs.server.SendError(chargingStationID, requestId, ocppError.Code, ocppError.Description, nil) + } else { + err = cs.server.SendError(chargingStationID, 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(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 || 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 confirmation response + err = cs.server.SendResponse(chargingStationID, requestId, response) + if err != nil { + // 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) + } +} + +func (cs *csms) notImplementedError(chargingStationID string, requestId string, action string) { + err := cs.server.SendError(chargingStationID, requestId, ocppj.NotImplemented, fmt.Sprintf("no handler for action %v implemented", action), nil) + if err != nil { + err = fmt.Errorf("replying cs %s to request %s with 'not implemented': %w", chargingStationID, requestId, err) + cs.error(err) + } +} + +func (cs *csms) notSupportedError(chargingStationID string, requestId string, action string) { + err := cs.server.SendError(chargingStationID, requestId, ocppj.NotSupported, fmt.Sprintf("unsupported action %v on CSMS", action), nil) + if err != nil { + err = fmt.Errorf("replying cs %s to request %s with 'not supported': %w", chargingStationID, requestId, err) + cs.error(err) + } +} + +func (cs *csms) handleIncomingRequest(chargingStation ChargingStationConnection, request ocpp.Request, requestId string, action string) { + profile, found := cs.server.GetProfileForFeature(action) + // Check whether action is supported and a listener for it exists + if !found { + cs.notImplementedError(chargingStation.ID(), requestId, action) + return + } else { + supported := true + switch profile.Name { + case authorization.ProfileName: + if cs.authorizationHandler == nil { + supported = false + } + case availability.ProfileName: + if cs.availabilityHandler == nil { + supported = false + } + case data.ProfileName: + if cs.dataHandler == nil { + supported = false + } + case diagnostics.ProfileName: + if cs.diagnosticsHandler == nil { + supported = false + } + case display.ProfileName: + if cs.displayHandler == nil { + supported = false + } + case firmware.ProfileName: + if cs.firmwareHandler == nil { + supported = false + } + case iso15118.ProfileName: + if cs.iso15118Handler == nil { + supported = false + } + case localauth.ProfileName: + if cs.localAuthListHandler == nil { + supported = false + } + case meter.ProfileName: + if cs.meterHandler == nil { + supported = false + } + case provisioning.ProfileName: + if cs.provisioningHandler == nil { + supported = false + } + case remotecontrol.ProfileName: + if cs.remoteControlHandler == nil { + supported = false + } + case reservation.ProfileName: + if cs.reservationHandler == nil { + supported = false + } + case security.ProfileName: + if cs.securityHandler == nil { + supported = false + } + case smartcharging.ProfileName: + if cs.smartChargingHandler == nil { + supported = false + } + case tariffcost.ProfileName: + if cs.tariffCostHandler == nil { + supported = false + } + case transactions.ProfileName: + if cs.transactionsHandler == nil { + supported = false + } + } + if !supported { + cs.notSupportedError(chargingStation.ID(), requestId, action) + return + } + } + var response ocpp.Response + var err error + // Execute in separate goroutine, so the caller goroutine is available + go func() { + switch action { + case provisioning.BootNotificationFeatureName: + response, err = cs.provisioningHandler.OnBootNotification(chargingStation.ID(), request.(*provisioning.BootNotificationRequest)) + case authorization.AuthorizeFeatureName: + response, err = cs.authorizationHandler.OnAuthorize(chargingStation.ID(), request.(*authorization.AuthorizeRequest)) + case smartcharging.ClearedChargingLimitFeatureName: + response, err = cs.smartChargingHandler.OnClearedChargingLimit(chargingStation.ID(), request.(*smartcharging.ClearedChargingLimitRequest)) + case data.DataTransferFeatureName: + response, err = cs.dataHandler.OnDataTransfer(chargingStation.ID(), request.(*data.DataTransferRequest)) + case firmware.FirmwareStatusNotificationFeatureName: + response, err = cs.firmwareHandler.OnFirmwareStatusNotification(chargingStation.ID(), request.(*firmware.FirmwareStatusNotificationRequest)) + case iso15118.Get15118EVCertificateFeatureName: + response, err = cs.iso15118Handler.OnGet15118EVCertificate(chargingStation.ID(), request.(*iso15118.Get15118EVCertificateRequest)) + case iso15118.GetCertificateStatusFeatureName: + response, err = cs.iso15118Handler.OnGetCertificateStatus(chargingStation.ID(), request.(*iso15118.GetCertificateStatusRequest)) + case availability.HeartbeatFeatureName: + response, err = cs.availabilityHandler.OnHeartbeat(chargingStation.ID(), request.(*availability.HeartbeatRequest)) + case diagnostics.LogStatusNotificationFeatureName: + response, err = cs.diagnosticsHandler.OnLogStatusNotification(chargingStation.ID(), request.(*diagnostics.LogStatusNotificationRequest)) + case meter.MeterValuesFeatureName: + response, err = cs.meterHandler.OnMeterValues(chargingStation.ID(), request.(*meter.MeterValuesRequest)) + case smartcharging.NotifyChargingLimitFeatureName: + response, err = cs.smartChargingHandler.OnNotifyChargingLimit(chargingStation.ID(), request.(*smartcharging.NotifyChargingLimitRequest)) + case diagnostics.NotifyCustomerInformationFeatureName: + response, err = cs.diagnosticsHandler.OnNotifyCustomerInformation(chargingStation.ID(), request.(*diagnostics.NotifyCustomerInformationRequest)) + case display.NotifyDisplayMessagesFeatureName: + response, err = cs.displayHandler.OnNotifyDisplayMessages(chargingStation.ID(), request.(*display.NotifyDisplayMessagesRequest)) + case smartcharging.NotifyEVChargingNeedsFeatureName: + response, err = cs.smartChargingHandler.OnNotifyEVChargingNeeds(chargingStation.ID(), request.(*smartcharging.NotifyEVChargingNeedsRequest)) + case smartcharging.NotifyEVChargingScheduleFeatureName: + response, err = cs.smartChargingHandler.OnNotifyEVChargingSchedule(chargingStation.ID(), request.(*smartcharging.NotifyEVChargingScheduleRequest)) + case diagnostics.NotifyEventFeatureName: + response, err = cs.diagnosticsHandler.OnNotifyEvent(chargingStation.ID(), request.(*diagnostics.NotifyEventRequest)) + case diagnostics.NotifyMonitoringReportFeatureName: + response, err = cs.diagnosticsHandler.OnNotifyMonitoringReport(chargingStation.ID(), request.(*diagnostics.NotifyMonitoringReportRequest)) + case provisioning.NotifyReportFeatureName: + response, err = cs.provisioningHandler.OnNotifyReport(chargingStation.ID(), request.(*provisioning.NotifyReportRequest)) + case firmware.PublishFirmwareStatusNotificationFeatureName: + response, err = cs.firmwareHandler.OnPublishFirmwareStatusNotification(chargingStation.ID(), request.(*firmware.PublishFirmwareStatusNotificationRequest)) + case smartcharging.ReportChargingProfilesFeatureName: + response, err = cs.smartChargingHandler.OnReportChargingProfiles(chargingStation.ID(), request.(*smartcharging.ReportChargingProfilesRequest)) + case reservation.ReservationStatusUpdateFeatureName: + response, err = cs.reservationHandler.OnReservationStatusUpdate(chargingStation.ID(), request.(*reservation.ReservationStatusUpdateRequest)) + case security.SecurityEventNotificationFeatureName: + response, err = cs.securityHandler.OnSecurityEventNotification(chargingStation.ID(), request.(*security.SecurityEventNotificationRequest)) + case security.SignCertificateFeatureName: + response, err = cs.securityHandler.OnSignCertificate(chargingStation.ID(), request.(*security.SignCertificateRequest)) + case availability.StatusNotificationFeatureName: + response, err = cs.availabilityHandler.OnStatusNotification(chargingStation.ID(), request.(*availability.StatusNotificationRequest)) + case transactions.TransactionEventFeatureName: + response, err = cs.transactionsHandler.OnTransactionEvent(chargingStation.ID(), request.(*transactions.TransactionEventRequest)) + default: + cs.notSupportedError(chargingStation.ID(), requestId, action) + return + } + cs.sendResponse(chargingStation.ID(), response, err, requestId) + }() +} + +func (cs *csms) handleIncomingResponse(chargingStation ChargingStationConnection, response ocpp.Response, requestId string) { + if callback, ok := cs.callbackQueue.Dequeue(chargingStation.ID()); ok { + // Execute in separate goroutine, so the caller goroutine is available + go callback(response, nil) + } else { + err := fmt.Errorf("no handler available for call of type %v from client %s for request %s", response.GetFeatureName(), chargingStation.ID(), requestId) + cs.error(err) + } +} + +func (cs *csms) handleIncomingError(chargingStation ChargingStationConnection, err *ocpp.Error, details interface{}) { + if callback, ok := cs.callbackQueue.Dequeue(chargingStation.ID()); ok { + // Execute in separate goroutine, so the caller goroutine is available + go callback(nil, err) + } else { + cs.error(fmt.Errorf("no handler available for call error %w from client %s", err, chargingStation.ID())) + } +} + +func (cs *csms) handleCanceledRequest(chargePointID string, request ocpp.Request, err *ocpp.Error) { + if callback, ok := cs.callbackQueue.Dequeue(chargePointID); ok { + // Execute in separate goroutine, so the caller goroutine is available + go callback(nil, err) + } else { + err := fmt.Errorf("no handler available for canceled request %s for client %s: %w", + request.GetFeatureName(), chargePointID, err) + cs.error(err) + } +} diff --git a/ocpp2.1/data/data.go b/ocpp2.1/data/data.go new file mode 100644 index 00000000..fa74f6fa --- /dev/null +++ b/ocpp2.1/data/data.go @@ -0,0 +1,23 @@ +// The data transfer functional block enables parties to add custom commands and extensions to OCPP 2.0. +package data + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Data transfer profile. +type CSMSHandler interface { + // OnDataTransfer is called on the CSMS whenever a DataTransferRequest is received from a charging station. + OnDataTransfer(chargingStationID string, request *DataTransferRequest) (confirmation *DataTransferResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Data transfer profile. +type ChargingStationHandler interface { + // OnDataTransfer is called on a charging station whenever a DataTransferRequest is received from the CSMS. + OnDataTransfer(request *DataTransferRequest) (confirmation *DataTransferResponse, err error) +} + +const ProfileName = "data" + +var Profile = ocpp.NewProfile( + ProfileName, + DataTransferFeature{}, +) diff --git a/ocpp2.1/data/data_transfer.go b/ocpp2.1/data/data_transfer.go new file mode 100644 index 00000000..70d4b491 --- /dev/null +++ b/ocpp2.1/data/data_transfer.go @@ -0,0 +1,86 @@ +package data + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Data Transfer (CS -> CSMS / CSMS -> CS) -------------------- + +const DataTransferFeatureName = "DataTransfer" + +// Status in DataTransferResponse messages. +type DataTransferStatus string + +const ( + DataTransferStatusAccepted DataTransferStatus = "Accepted" + DataTransferStatusRejected DataTransferStatus = "Rejected" + DataTransferStatusUnknownMessageId DataTransferStatus = "UnknownMessageId" + DataTransferStatusUnknownVendorId DataTransferStatus = "UnknownVendorId" +) + +func isValidDataTransferStatus(fl validator.FieldLevel) bool { + status := DataTransferStatus(fl.Field().String()) + switch status { + case DataTransferStatusAccepted, DataTransferStatusRejected, DataTransferStatusUnknownMessageId, DataTransferStatusUnknownVendorId: + return true + default: + return false + } +} + +// The field definition of the DataTransfer request payload sent by an endpoint to ther other endpoint. +type DataTransferRequest struct { + MessageID string `json:"messageId,omitempty" validate:"max=50"` + Data interface{} `json:"data,omitempty"` + VendorID string `json:"vendorId" validate:"required,max=255"` +} + +// This field definition of the DataTransfer response payload, sent by an endpoint in response to a DataTransferRequest, coming from the other endpoint. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type DataTransferResponse struct { + Status DataTransferStatus `json:"status" validate:"required,dataTransferStatus21"` + Data interface{} `json:"data,omitempty"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// If a CS needs to send information to the CSMS for a function not supported by OCPP, it SHALL use a DataTransfer message. +// The same functionality may also be offered the other way around, allowing a CSMS to send arbitrary custom commands to a CS. +type DataTransferFeature struct{} + +func (f DataTransferFeature) GetFeatureName() string { + return DataTransferFeatureName +} + +func (f DataTransferFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(DataTransferRequest{}) +} + +func (f DataTransferFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(DataTransferResponse{}) +} + +func (r DataTransferRequest) GetFeatureName() string { + return DataTransferFeatureName +} + +func (c DataTransferResponse) GetFeatureName() string { + return DataTransferFeatureName +} + +// Creates a new DataTransferRequest, containing all required fields. Optional fields may be set afterwards. +func NewDataTransferRequest(vendorId string) *DataTransferRequest { + return &DataTransferRequest{VendorID: vendorId} +} + +// Creates a new DataTransferResponse. Optional fields may be set afterwards. +func NewDataTransferResponse(status DataTransferStatus) *DataTransferResponse { + return &DataTransferResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("dataTransferStatus21", isValidDataTransferStatus) +} diff --git a/ocpp2.1/der/clear_der_control.go b/ocpp2.1/der/clear_der_control.go new file mode 100644 index 00000000..506dd5f6 --- /dev/null +++ b/ocpp2.1/der/clear_der_control.go @@ -0,0 +1,59 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- ClearDERControl (CSMS -> CS) -------------------- + +const ClearDERControl = "ClearDERControl" + +// The field definition of the ClearDERControlRequest request payload sent by the CSMS to the Charging Station. +type ClearDERControlRequest struct { + IsDefault bool `json:"isDefault" validate:"required"` + ControlType *DERControl `json:"controlType,omitempty" validate:"omitempty"` + ControlId string `json:"controlId,omitempty" validate:"omitempty,max=36"` +} + +// This field definition of the ClearDERControlResponse +type ClearDERControlResponse struct { + Status DERControlStatus `json:"status" validate:"required,derControlStatus"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +type ClearDERControlFeature struct{} + +func (f ClearDERControlFeature) GetFeatureName() string { + return ClearDERControl +} + +func (f ClearDERControlFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearDERControlRequest{}) +} + +func (f ClearDERControlFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearDERControlResponse{}) +} + +func (r ClearDERControlRequest) GetFeatureName() string { + return ClearDERControl +} + +func (c ClearDERControlResponse) GetFeatureName() string { + return ClearDERControl +} + +// Creates a new ClearDERControlRequest, containing all required fields. Optional fields may be set afterwards. +func NewClearDERControlRequest(isDefault bool) *ClearDERControlRequest { + return &ClearDERControlRequest{ + IsDefault: isDefault, + } +} + +// Creates a new ClearDERControlResponse, containing all required fields. Optional fields may be set afterwards. +func NewClearDERControlResponse(status DERControlStatus) *ClearDERControlResponse { + return &ClearDERControlResponse{ + Status: status, + } +} diff --git a/ocpp2.1/der/der.go b/ocpp2.1/der/der.go new file mode 100644 index 00000000..b650dbbe --- /dev/null +++ b/ocpp2.1/der/der.go @@ -0,0 +1,31 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" +) + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 DER profile. +type CSMSHandler interface { + OnNotifyDERStartStop(chargingStationId string, req *NotifyDERStartStopRequest) (res *NotifyDERStartStopResponse, err error) + OnNotifyDERAlarm(chargingStationId string, req *NotifyDERAlarmRequest) (res *NotifyDERAlarmResponse, err error) + OnReportDERControl(chargingStationId string, req *ReportDERControlRequest) (res *ReportDERControlResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 DER profile. +type ChargingStationHandler interface { + OnGetDERControl(chargingStationId string, req *GetDERControlRequest) (res *GetDERControlResponse, err error) + OnSetDERControl(chargingStationId string, req *SetDERControlRequest) (res *SetDERControlResponse, err error) + OnClearDERControl(chargingStationId string, req *ClearDERControlRequest) (res *ClearDERControlResponse, err error) +} + +const ProfileName = "DER" + +var Profile = ocpp.NewProfile( + ProfileName, + GetDERControlFeature{}, + SetDERControlFeature{}, + ClearDERControlFeature{}, + NotifyDERStartStopFeature{}, + NotifyDERAlarmFeature{}, + ReportDERControlFeature{}, +) diff --git a/ocpp2.1/der/get_der_control.go b/ocpp2.1/der/get_der_control.go new file mode 100644 index 00000000..15de8ce8 --- /dev/null +++ b/ocpp2.1/der/get_der_control.go @@ -0,0 +1,60 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- GetDERControl (CSMS -> CS) -------------------- + +const GetDERControl = "GetDERControl" + +// The field definition of the GetDERControlRequest request payload sent by the CSMS to the Charging Station. +type GetDERControlRequest struct { + RequestId int `json:"requestId" validate:"required"` + IsDefault *bool `json:"isDefault,omitempty" validate:"omitempty"` + ControlType *DERControl `json:"controlType,omitempty" validate:"omitempty"` + ControlId string `json:"controlId,omitempty" validate:"omitempty,max=36"` // Optional field, max length 36 characters +} + +// This field definition of the GetDERControlResponse +type GetDERControlResponse struct { + Status DERControlStatus `json:"status" validate:"required,derControlStatus"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +type GetDERControlFeature struct{} + +func (f GetDERControlFeature) GetFeatureName() string { + return GetDERControl +} + +func (f GetDERControlFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetDERControlRequest{}) +} + +func (f GetDERControlFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetDERControlResponse{}) +} + +func (r GetDERControlRequest) GetFeatureName() string { + return GetDERControl +} + +func (c GetDERControlResponse) GetFeatureName() string { + return GetDERControl +} + +// Creates a new GetDERControlRequest, containing all required fields. Optional fields may be set afterwards. +func NewGetDERControlResponseRequest(requestId int) *GetDERControlRequest { + return &GetDERControlRequest{ + RequestId: requestId, + } +} + +// Creates a new GetDERControlResponse, containing all required fields. Optional fields may be set afterwards. +func NewGetDERControlResponseResponse(status DERControlStatus) *GetDERControlResponse { + return &GetDERControlResponse{ + Status: status, + } +} diff --git a/ocpp2.1/der/notify_der_alarm.go b/ocpp2.1/der/notify_der_alarm.go new file mode 100644 index 00000000..f31ddf92 --- /dev/null +++ b/ocpp2.1/der/notify_der_alarm.go @@ -0,0 +1,58 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "reflect" +) + +// -------------------- NotifyDERAlarm (CS -> CSMS) -------------------- + +const NotifyDERAlarm = "NotifyDERAlarm" + +// The field definition of the NotifyDERAlarmRequest request payload sent by the CSMS to the Charging Station. +type NotifyDERAlarmRequest struct { + ControlType DERControl `json:"controlType" validate:"required"` + GridEventFault GridEventFault `json:"gridEventFault,omitempty" validate:"omitempty,gridEventFault"` + AlarmEnded *bool `json:"alarmEnded,omitempty" validate:"omitempty"` + Timestamp types.DateTime `json:"timestamp" validate:"required"` + ExtraInfo string `json:"extraInfo,omitempty" validate:"omitempty,max=200"` +} + +// This field definition of the NotifyDERAlarmResponse +type NotifyDERAlarmResponse struct { +} + +type NotifyDERAlarmFeature struct{} + +func (f NotifyDERAlarmFeature) GetFeatureName() string { + return NotifyDERAlarm +} + +func (f NotifyDERAlarmFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyDERAlarmRequest{}) +} + +func (f NotifyDERAlarmFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyDERAlarmResponse{}) +} + +func (r NotifyDERAlarmRequest) GetFeatureName() string { + return NotifyDERAlarm +} + +func (c NotifyDERAlarmResponse) GetFeatureName() string { + return NotifyDERAlarm +} + +// Creates a new NotifyDERAlarmRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyDERAlarmRequest(controlType DERControl, timestamp types.DateTime) *NotifyDERAlarmRequest { + return &NotifyDERAlarmRequest{ + ControlType: controlType, + Timestamp: timestamp, + } +} + +// Creates a new NewAFFRSignalResponse, containing all required fields. Optional fields may be set afterwards. +func NewNotifyDERAlarmResponse() *NotifyDERAlarmResponse { + return &NotifyDERAlarmResponse{} +} diff --git a/ocpp2.1/der/notify_der_start_stop.go b/ocpp2.1/der/notify_der_start_stop.go new file mode 100644 index 00000000..daf0f54e --- /dev/null +++ b/ocpp2.1/der/notify_der_start_stop.go @@ -0,0 +1,62 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "reflect" +) + +// -------------------- NotifyDERStartStop (CSMS -> CS) -------------------- + +const NotifyDERStartStop = "NotifyDERStartStop" + +// The field definition of the NotifyDERStartStopRequest request payload sent by the CSMS to the Charging Station. +type NotifyDERStartStopRequest struct { + ControlId string `json:"controlId" validate:"required,max=36"` + Started bool `json:"started" validate:"required"` // Indicates whether the DER is started or stopped. + Timestamp types.DateTime `json:"timestamp" validate:"required"` + SupersededIds []string `json:"supersededIds,omitempty" validate:"omitempty,max=24"` +} + +// This field definition of the NotifyDERStartStopResponse +type NotifyDERStartStopResponse struct { +} + +type NotifyDERStartStopFeature struct{} + +func (f NotifyDERStartStopFeature) GetFeatureName() string { + return NotifyDERStartStop +} + +func (f NotifyDERStartStopFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyDERStartStopRequest{}) +} + +func (f NotifyDERStartStopFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyDERStartStopResponse{}) +} + +func (r NotifyDERStartStopRequest) GetFeatureName() string { + return NotifyDERStartStop +} + +func (c NotifyDERStartStopResponse) GetFeatureName() string { + return NotifyDERStartStop +} + +// Creates a new NotifyDERStartStopRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyDERStartStopRequest( + controlId string, + started bool, + timestamp types.DateTime, +) *NotifyDERStartStopRequest { + return &NotifyDERStartStopRequest{ + ControlId: controlId, + Started: started, + Timestamp: timestamp, + } +} + +// Creates a new NotifyDERStartStopResponse, containing all required fields. Optional fields may be set afterwards. +func NewNotifyDERStartStopResponse() *NotifyDERStartStopResponse { + return &NotifyDERStartStopResponse{} +} diff --git a/ocpp2.1/der/report_der_control.go b/ocpp2.1/der/report_der_control.go new file mode 100644 index 00000000..7a0364f9 --- /dev/null +++ b/ocpp2.1/der/report_der_control.go @@ -0,0 +1,61 @@ +package der + +import ( + "reflect" +) + +// -------------------- ReportDERControl (CS -> CSMS) -------------------- + +const ReportDERControl = "ReportDERControl" + +// The field definition of the ReportDERControlRequest request payload sent by the CSMS to the Charging Station. +type ReportDERControlRequest struct { + RequestId int `json:"requestId" validate:"required"` + Tbc *bool `json:"tbc,omitempty" validate:"omitempty"` + FixedPFAbsorb []FixedPFGet `json:"fixedPFAbsorb,omitempty" validate:"omitempty,max=24,dive"` + FixedPFInject []FixedPFGet `json:"fixedPFInject,omitempty" validate:"omitempty,max=24,dive"` + FixedVar []FixedVarGet `json:"fixedVar,omitempty" validate:"omitempty,max=24,dive"` + LimitMaxDischarge []LimitMaxDischargeGet `json:"limitMaxDischarge,omitempty" validate:"omitempty,max=24,dive"` + FreqDroop []FreqDroopGet `json:"freqDroop,omitempty" validate:"omitempty,max=24,dive"` + EnterService []EnterServiceGet `json:"enterService,omitempty" validate:"omitempty,max=24,dive"` + Gradient []GradientGet `json:"gradient,omitempty" validate:"omitempty,max=24,dive"` + Curve []DERCurveGet `json:"curve,omitempty" validate:"omitempty,max=24,dive"` +} + +// This field definition of the ReportDERControlResponse +type ReportDERControlResponse struct { +} + +type ReportDERControlFeature struct{} + +func (f ReportDERControlFeature) GetFeatureName() string { + return ReportDERControl +} + +func (f ReportDERControlFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ReportDERControlRequest{}) +} + +func (f ReportDERControlFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ReportDERControlResponse{}) +} + +func (r ReportDERControlRequest) GetFeatureName() string { + return ReportDERControl +} + +func (c ReportDERControlResponse) GetFeatureName() string { + return ReportDERControl +} + +// Creates a new ReportDERControlRequest, containing all required fields. Optional fields may be set afterwards. +func NewReportDERControlRequest(requestId int) *ReportDERControlRequest { + return &ReportDERControlRequest{ + RequestId: requestId, + } +} + +// Creates a new ReportDERControlResponse, containing all required fields. Optional fields may be set afterwards. +func NewReportDERControlResponse() *ReportDERControlResponse { + return &ReportDERControlResponse{} +} diff --git a/ocpp2.1/der/set_der_control.go b/ocpp2.1/der/set_der_control.go new file mode 100644 index 00000000..c3adb2e1 --- /dev/null +++ b/ocpp2.1/der/set_der_control.go @@ -0,0 +1,69 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- SetDERControl (CSMS -> CS) -------------------- + +const SetDERControl = "SetDERControl" + +// The field definition of the SetDERControlRequest request payload sent by the CSMS to the Charging Station. +type SetDERControlRequest struct { + IsDefault bool `json:"isDefault" validate:"required"` // Indicates whether the DER control is set to default values. + ControlID string `json:"controlId" validate:"required"` // The unique identifier of the DER control to be set. + ControlType DERControl `json:"controlType" validate:"required,derControl"` + Curve *DERCurve `json:"curve,omitempty" validate:"omitempty,dive"` + Gradient *Gradient `json:"gradient,omitempty" validate:"omitempty,dive"` + FreqDroop *FreqDroop `json:"freqDroop,omitempty" validate:"omitempty,dive"` + FixedPFAbsorb *FixedPF `json:"fixedPFAbsorb,omitempty" validate:"omitempty,dive"` + FixedPFInject *FixedPF `json:"fixedPFInject,omitempty" validate:"omitempty,dive"` + LimitMaxDischarge *LimitMaxDischarge `json:"limitMaxDischarge,omitempty" validate:"omitempty,dive"` + EnterService *EnterService `json:"enterService,omitempty" validate:"omitempty,dive"` +} + +// This field definition of the SetDERControlResponse +type SetDERControlResponse struct { + Status DERControlStatus `json:"status" validate:"required,derControlStatus"` + SuperseededIds []string `json:"superseededIds,omitempty" validate:"omitempty,max=24"` + StatusInfo types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +type SetDERControlFeature struct{} + +func (f SetDERControlFeature) GetFeatureName() string { + return SetDERControl +} + +func (f SetDERControlFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetDERControlRequest{}) +} + +func (f SetDERControlFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetDERControlResponse{}) +} + +func (r SetDERControlRequest) GetFeatureName() string { + return SetDERControl +} + +func (c SetDERControlResponse) GetFeatureName() string { + return SetDERControl +} + +// Creates a new SetDERControlRequest, containing all required fields. Optional fields may be set afterwards. +func NewSetDERControlResponseRequest(isDefault bool, controlId string, controlType DERControl) *SetDERControlRequest { + return &SetDERControlRequest{ + IsDefault: isDefault, + ControlID: controlId, + ControlType: controlType, + } +} + +// Creates a new SetDERControlResponse, containing all required fields. Optional fields may be set afterwards. +func NewSetDERControlResponseResponse(status DERControlStatus) *SetDERControlResponse { + return &SetDERControlResponse{ + Status: status, + } +} diff --git a/ocpp2.1/der/types.go b/ocpp2.1/der/types.go new file mode 100644 index 00000000..86938c08 --- /dev/null +++ b/ocpp2.1/der/types.go @@ -0,0 +1,280 @@ +package der + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" +) + +type DERControlStatus string + +const ( + DERControlStatusAccepted = "Accepted" + DERControlStatusRejected = "Rejected" + DERControlStatusNotSupported = "NotSupported" + DERControlStatusNotFound = "NotFound" +) + +func isValidDERControlStatus(level validator.FieldLevel) bool { + switch DERControlStatus(level.Field().String()) { + case DERControlStatusAccepted, DERControlStatusRejected, DERControlStatusNotSupported, DERControlStatusNotFound: + return true + default: + return false + } +} + +type DERControl string + +const ( + DERControlEnterService = "EnterService" + DERControlFreqDroop = "FreqDroop" + DERControlFreqWatt = "FreqWatt" + DERControlFixedPFAbsorb = "FixedPFAbsorb" + DERControlFixedPFInject = "FixedPFInject" + DERControlFixedVar = "FixedVar" + DERControlGradients = "Gradients" + DERControlHFMustTrip = "HFMustTrip" + DERControlHFMayTrip = "HFMayTrip" + DERControlHVMustTrip = "HVMustTrip" + DERControlHVMomCess = "HVMomCess" + DERControlHVMayTrip = "HVMayTrip" + DERControlLimitMaxDischarge = "LimitMaxDischarge" + DERControlLFMustTrip = "LFMustTrip" + DERControlLVMustTrip = "LVMustTrip" + DERControlLVMomCess = "LVMomCess" + DERControlLVMayTrip = "LVMayTrip" + DERControlPowerMonitoringMustTrip = "PowerMonitoringMustTrip" + DERControlVoltVar = "VoltVar" + DERControlVoltWatt = "VoltWatt" + DERControlWattPF = "WattPF" + DERControlWattVar = "WattVar" +) + +func isValidDERControl(level validator.FieldLevel) bool { + switch DERControl(level.Field().String()) { + case DERControlEnterService, DERControlFreqDroop, DERControlFreqWatt, DERControlFixedPFAbsorb, DERControlFixedPFInject, + DERControlFixedVar, DERControlGradients, DERControlHFMustTrip, DERControlHFMayTrip, DERControlHVMustTrip, + DERControlHVMomCess, DERControlHVMayTrip, DERControlLimitMaxDischarge, DERControlLFMustTrip, DERControlLVMustTrip, + DERControlLVMomCess, DERControlLVMayTrip, DERControlPowerMonitoringMustTrip, DERControlVoltVar, + DERControlVoltWatt, DERControlWattPF, DERControlWattVar: + return true + default: + return false + } +} + +type DERUnit string + +const ( + DERUnitNotApplicable = "Not_Applicable" + DERUnitPctMaxW = "PctMaxW" + DERUnitPctMaxVar = "PctMaxVar" + DERUnitPctWAvail = "PctWAvail" + DERUnitPctVarAvail = "PctVarAvail" + DERUnitPctEffectiveV = "PctEffectiveV" +) + +func isValidDERUnit(level validator.FieldLevel) bool { + switch DERUnit(level.Field().String()) { + case DERUnitNotApplicable, DERUnitPctMaxW, DERUnitPctMaxVar, DERUnitPctWAvail, DERUnitPctVarAvail, DERUnitPctEffectiveV: + return true + default: + return false + } +} + +func init() { + _ = ocppj.Validate.RegisterValidation("derUnit", isValidDERUnit) + _ = ocppj.Validate.RegisterValidation("derControlStatus", isValidDERControlStatus) + _ = ocppj.Validate.RegisterValidation("derControl", isValidDERControl) + _ = ocppj.Validate.RegisterValidation("powerDuringCessation", isValidPowerDuringCessation) +} + +type DERCurve struct { + Priority int `json:"priority" validate:"required,gte=0"` + YUnit DERUnit `json:"yUnit" validate:"required,derUnit"` + ResponseTime *float64 `json:"responseTime,omitempty" validate:"omitempty"` + StartTime *types.DateTime `json:"startTime,omitempty" validate:"omitempty"` + Duration float64 `json:"duration,omitempty" validate:"omitempty"` + Hysteresis *Hysteresis `json:"hysteresis,omitempty" validate:"omitempty,dive"` + VoltageParams *VoltageParams `json:"voltageParams,omitempty" validate:"omitempty,dive"` + ReactivePowerParams *ReactivePowerParams `json:"reactivePowerParams,omitempty" validate:"omitempty,dive"` + CurveData []DERCurvePoints `json:"curveData" validate:"required,gte=1,max=10,dive"` +} + +type DERCurvePoints struct { + X float64 `json:"x" validate:"required"` // X value of the curve point, e.g., frequency or voltage. + Y float64 `json:"y" validate:"required"` // Y value of the curve point, e.g., active or reactive power. +} + +type DERCurveGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + DERCurve DERCurve `json:"derCurve" validate:"required,dive"` +} + +type Hysteresis struct { + HysteresisHigh *float64 `json:"hysteresisHigh,omitempty" validate:"omitempty"` + HysteresisLow *float64 `json:"hysteresisLow,omitempty" validate:"omitempty"` + HysteresisDelay *float64 `json:"hysteresisDelay,omitempty" validate:"omitempty"` + HysteresisGradient *float64 `json:"hysteresisGradient,omitempty" validate:"omitempty"` +} + +type PowerDuringCessation string + +const ( + PowerDuringCessationActive PowerDuringCessation = "Active" + PowerDuringCessationReactive PowerDuringCessation = "Reactive" +) + +func isValidPowerDuringCessation(level validator.FieldLevel) bool { + switch PowerDuringCessation(level.Field().String()) { + case PowerDuringCessationActive, PowerDuringCessationReactive: + return true + default: + return false + } +} + +type VoltageParams struct { + Hv10MinMeanValue *float64 `json:"hv10MinMeanValue,omitempty" validate:"omitempty"` + Hv10MinMeanTripDelay *float64 `json:"hv10MinMeanTripDelay,omitempty" validate:"omitempty"` + PowerDuringCessation *PowerDuringCessation `json:"powerDuring,omitempty" validate:"omitempty,powerDuringCessation"` +} + +type ReactivePowerParams struct { + VRef *float64 `json:"vRef,omitempty" validate:"omitempty"` + AutonomousVRefEnable *bool `json:"autonomousVRefEnable,omitempty" validate:"omitempty"` + AutonomousVRefTimeConstant *float64 `json:"autonomousVRefTimeConstant,omitempty" validate:"omitempty"` +} + +type Gradient struct { + Priority int `json:"priority" validate:"required,gte=0"` + Gradient float64 `json:"gradient" validate:"required"` + SoftGradient float64 `json:"softGradient" validate:"required"` +} + +type GradientGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + Gradient Gradient `json:"gradient" validate:"required,dive"` +} + +type FreqDroop struct { + Priority int `json:"priority" validate:"required,gte=0"` + OverFrequency float64 `json:"overFreq" validate:"required"` + UnderFrequency float64 `json:"underFreq" validate:"required"` + OverDroop float64 `json:"overDroop" validate:"required"` + UnderDroop float64 `json:"underDroop" validate:"required"` + ResponseTime *float64 `json:"responseTime,omitempty" validate:"omitempty"` + Duration *float64 `json:"duration,omitempty" validate:"omitempty"` +} + +type FreqDroopGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + FreqDroop FreqDroop `json:"freqDroop" validate:"required,dive"` +} + +type LimitMaxDischarge struct { + Priority int `json:"priority" validate:"required,gte=0"` + PctMaxDischargePower float64 `json:"pctMaxDischargePower,omitempty" validate:"omitempty"` // Percentage of maximum discharge power. + StartTime *types.DateTime `json:"startTime,omitempty" validate:"omitempty"` // The time at which the limit starts. + Duration *float64 `json:"duration,omitempty" validate:"omitempty"` // Duration of the limit in seconds. + PowerMonitoringMustTrip *DERCurve `json:"powerMonitoringMustTrip,omitempty" validate:"omitempty,dive"` // Optional DER curve for power monitoring must trip. +} +type LimitMaxDischargeGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + LimitMaxDischarge LimitMaxDischarge `json:"limitMaxDischarge" validate:"required,dive"` +} + +type EnterService struct { + Priority int `json:"priority" validate:"required,gte=0"` + HighVoltage float64 `json:"highVoltage" validate:"required"` + LowVoltage float64 `json:"lowVoltage" validate:"required"` + HighFrequency float64 `json:"highFreq" validate:"required"` + LowFrequency float64 `json:"lowFreq" validate:"required"` + Delay *float64 `json:"delay,omitempty" validate:"omitempty"` + RandomDelay *float64 `json:"randomDelay,omitempty" validate:"omitempty"` + RampRate *float64 `json:"rampRate,omitempty" validate:"omitempty"` +} +type EnterServiceGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + EnterService EnterService `json:"enterService" validate:"required,dive"` +} + +type FixedPF struct { + Priority int `json:"priority" validate:"required,gte=0"` + Displacement float64 `json:"displacement" validate:"required"` + Excitation bool `json:"excitation" validate:"required"` + StartTime *types.DateTime `json:"startTime,omitempty" validate:"omitempty"` + Duration *float64 `json:"duration,omitempty" validate:"omitempty"` +} + +type FixedPFGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + FixedPF FixedPF `json:"fixedPF" validate:"required,dive"` +} + +type FixedVar struct { + Priority int `json:"priority" validate:"required,gte=0"` + Setpoint float64 `json:"setpoint" validate:"required"` + Unit DERUnit `json:"unit" validate:"required,derUnit"` + StartTime *types.DateTime `json:"startTime,omitempty" validate:"omitempty"` + Duration *float64 `json:"duration,omitempty" validate:"omitempty"` +} + +type FixedVarGet struct { + Id string `json:"id" validate:"required,max=36"` + IsDefault bool `json:"isDefault" validate:"required"` + IsSuperseded bool `json:"isSuperseded" validate:"required"` + FixedVar FixedVar `json:"fixedVar" validate:"required,dive"` +} + +type GridEventFault string + +const ( + GridEventFaultOverVoltage GridEventFault = "OverVoltage" + GridEventFaultUnderVoltage GridEventFault = "UnderVoltage" + GridEventFaultOverFrequency GridEventFault = "OverFrequency" + GridEventFaultUnderFrequency GridEventFault = "UnderFrequency" + GridEventFaultVoltageImbalance GridEventFault = "VoltageImbalance" + GridEventFaultLowInputPower GridEventFault = "LowInputPower" + GridEventFaultOverCurrent GridEventFault = "OverCurrent" + GridEventFaultPhaseRotation GridEventFault = "PhaseRotation" + GridEventFaultRemoteEmergency GridEventFault = "RemoteEmergency" + GridEventFaultCurrentImbalance GridEventFault = "CurrentImbalance" +) + +func isValidGridEventFault(level validator.FieldLevel) bool { + switch GridEventFault(level.Field().String()) { + case GridEventFaultOverVoltage, + GridEventFaultUnderVoltage, + GridEventFaultOverFrequency, + GridEventFaultUnderFrequency, + GridEventFaultVoltageImbalance, + GridEventFaultLowInputPower, + GridEventFaultOverCurrent, + GridEventFaultPhaseRotation, + GridEventFaultRemoteEmergency, + GridEventFaultCurrentImbalance: + return true + default: + return false + } +} + +func init() { + // Register the custom validation function for GridEventFault + _ = ocppj.Validate.RegisterValidation("gridEventFault", isValidGridEventFault) +} diff --git a/ocpp2.1/diagnostics/clear_variable_monitoring.go b/ocpp2.1/diagnostics/clear_variable_monitoring.go new file mode 100644 index 00000000..c1b8d5a0 --- /dev/null +++ b/ocpp2.1/diagnostics/clear_variable_monitoring.go @@ -0,0 +1,86 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Clear Variable Monitoring (CSMS -> CS) -------------------- + +const ClearVariableMonitoringFeatureName = "ClearVariableMonitoring" + +// Status contained inside a ClearMonitoringResult struct. +type ClearMonitoringStatus string + +const ( + ClearMonitoringStatusAccepted ClearMonitoringStatus = "Accepted" + ClearMonitoringStatusRejected ClearMonitoringStatus = "Rejected" + ClearMonitoringStatusNotFound ClearMonitoringStatus = "NotFound" +) + +func isValidClearMonitoringStatus(fl validator.FieldLevel) bool { + status := ClearMonitoringStatus(fl.Field().String()) + switch status { + case ClearMonitoringStatusAccepted, ClearMonitoringStatusRejected, ClearMonitoringStatusNotFound: + return true + default: + return false + } +} + +type ClearMonitoringResult struct { + ID int `json:"id" validate:"required,gte=0"` + Status ClearMonitoringStatus `json:"status" validate:"required,clearMonitoringStatus21"` +} + +// The field definition of the ClearVariableMonitoring request payload sent by the CSMS to the Charging Station. +type ClearVariableMonitoringRequest struct { + ID []int `json:"id" validate:"required,min=1,dive,gte=0"` // List of the monitors to be cleared, identified by their Id. +} + +// This field definition of the ClearVariableMonitoring response payload, sent by the Charging Station to the CSMS in response to a ClearVariableMonitoringRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClearVariableMonitoringResponse struct { + ClearMonitoringResult []ClearMonitoringResult `json:"clearMonitoringResult" validate:"required,min=1,dive"` // List of result statuses per monitor. +} + +// The CSMS asks the Charging Station to clear/remove a display message that has been configured in the Charging Station. +// The Charging station checks for a message with the requested ID and removes it. +// The Charging station then responds with a ClearVariableMonitoringResponse. The response payload indicates whether the Charging Station was able to remove the message from display or not. +type ClearVariableMonitoringFeature struct{} + +func (f ClearVariableMonitoringFeature) GetFeatureName() string { + return ClearVariableMonitoringFeatureName +} + +func (f ClearVariableMonitoringFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearVariableMonitoringRequest{}) +} + +func (f ClearVariableMonitoringFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearVariableMonitoringResponse{}) +} + +func (r ClearVariableMonitoringRequest) GetFeatureName() string { + return ClearVariableMonitoringFeatureName +} + +func (c ClearVariableMonitoringResponse) GetFeatureName() string { + return ClearVariableMonitoringFeatureName +} + +// Creates a new ClearVariableMonitoringRequest, containing all required fields. There are no optional fields for this message. +func NewClearVariableMonitoringRequest(id []int) *ClearVariableMonitoringRequest { + return &ClearVariableMonitoringRequest{ID: id} +} + +// Creates a new ClearVariableMonitoringResponse, containing all required fields. There are no optional fields for this message. +func NewClearVariableMonitoringResponse(result []ClearMonitoringResult) *ClearVariableMonitoringResponse { + return &ClearVariableMonitoringResponse{ClearMonitoringResult: result} +} + +func init() { + _ = types.Validate.RegisterValidation("clearMonitoringStatus21", isValidClearMonitoringStatus) +} diff --git a/ocpp2.1/diagnostics/customer_information.go b/ocpp2.1/diagnostics/customer_information.go new file mode 100644 index 00000000..79769ddb --- /dev/null +++ b/ocpp2.1/diagnostics/customer_information.go @@ -0,0 +1,89 @@ +package diagnostics + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Customer Information (CSMS -> CS) -------------------- + +const CustomerInformationFeatureName = "CustomerInformation" + +// Status returned in response to CustomerInformationRequest. +type CustomerInformationStatus string + +const ( + CustomerInformationStatusAccepted CustomerInformationStatus = "Accepted" + CustomerInformationStatusRejected CustomerInformationStatus = "Rejected" + CustomerInformationStatusInvalid CustomerInformationStatus = "Invalid" +) + +func isValidCustomerInformationStatus(fl validator.FieldLevel) bool { + status := CustomerInformationStatus(fl.Field().String()) + switch status { + case CustomerInformationStatusAccepted, CustomerInformationStatusRejected, CustomerInformationStatusInvalid: + return true + default: + return false + } +} + +// The field definition of the CustomerInformation request payload sent by the CSMS to the Charging Station. +type CustomerInformationRequest struct { + RequestID int `json:"requestId" validate:"gte=0"` + Report bool `json:"report"` + Clear bool `json:"clear"` + CustomerIdentifier string `json:"customerIdentifier,omitempty" validate:"max=64"` + IdToken *types.IdToken `json:"idToken,omitempty" validate:"omitempty,dive"` + CustomerCertificate *types.CertificateHashData `json:"customerCertificate,omitempty" validate:"omitempty,dive"` +} + +// This field definition of the CustomerInformation response payload, sent by the Charging Station to the CSMS in response to a CustomerInformationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type CustomerInformationResponse struct { + Status CustomerInformationStatus `json:"status" validate:"required,customerInformationStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// CSMS can request a Charging Station to clear its Authorization Cache. +// The CSMS SHALL send a CustomerInformationRequest payload for clearing the Charging Station’s Authorization Cache. +// Upon receipt of a CustomerInformationRequest, the Charging Station SHALL respond with a CustomerInformationResponse payload. +// The response payload SHALL indicate whether the Charging Station was able to clear its Authorization Cache. +type CustomerInformationFeature struct{} + +func (f CustomerInformationFeature) GetFeatureName() string { + return CustomerInformationFeatureName +} + +func (f CustomerInformationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(CustomerInformationRequest{}) +} + +func (f CustomerInformationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(CustomerInformationResponse{}) +} + +func (r CustomerInformationRequest) GetFeatureName() string { + return CustomerInformationFeatureName +} + +func (c CustomerInformationResponse) GetFeatureName() string { + return CustomerInformationFeatureName +} + +// Creates a new CustomerInformationRequest, containing all required fields. Additional optional fields may be set afterwards. +func NewCustomerInformationRequest(requestId int, report bool, clear bool) *CustomerInformationRequest { + return &CustomerInformationRequest{RequestID: requestId, Report: report, Clear: clear} +} + +// Creates a new CustomerInformationResponse, containing all required fields. Additional optional fields may be set afterwards. +func NewCustomerInformationResponse(status CustomerInformationStatus) *CustomerInformationResponse { + return &CustomerInformationResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("customerInformationStatus21", isValidCustomerInformationStatus) +} diff --git a/ocpp2.1/diagnostics/diagnostics.go b/ocpp2.1/diagnostics/diagnostics.go new file mode 100644 index 00000000..78f3d080 --- /dev/null +++ b/ocpp2.1/diagnostics/diagnostics.go @@ -0,0 +1,51 @@ +// The diagnostics functional block contains OCPP 2.1 features than enable remote diagnostics of problems with a charging station. +package diagnostics + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Diagnostics profile. +type CSMSHandler interface { + // OnLogStatusNotification is called on the CSMS whenever a LogStatusNotificationRequest is received from a Charging Station. + OnLogStatusNotification(chargingStationID string, request *LogStatusNotificationRequest) (response *LogStatusNotificationResponse, err error) + // OnNotifyCustomerInformation is called on the CSMS whenever a NotifyCustomerInformationRequest is received from a Charging Station. + OnNotifyCustomerInformation(chargingStationID string, request *NotifyCustomerInformationRequest) (response *NotifyCustomerInformationResponse, err error) + // OnNotifyEvent is called on the CSMS whenever a NotifyEventRequest is received from a Charging Station. + OnNotifyEvent(chargingStationID string, request *NotifyEventRequest) (response *NotifyEventResponse, err error) + // OnNotifyMonitoringReport is called on the CSMS whenever a NotifyMonitoringReportRequest is received from a Charging Station. + OnNotifyMonitoringReport(chargingStationID string, request *NotifyMonitoringReportRequest) (response *NotifyMonitoringReportResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Diagnostics profile. +type ChargingStationHandler interface { + // OnClearVariableMonitoring is called on a charging station whenever a ClearVariableMonitoringRequest is received from the CSMS. + OnClearVariableMonitoring(request *ClearVariableMonitoringRequest) (response *ClearVariableMonitoringResponse, err error) + // OnCustomerInformation is called on a charging station whenever a CustomerInformationRequest is received from the CSMS. + OnCustomerInformation(request *CustomerInformationRequest) (response *CustomerInformationResponse, err error) + // OnGetLog is called on a charging station whenever a GetLogRequest is received from the CSMS. + OnGetLog(request *GetLogRequest) (response *GetLogResponse, err error) + // OnGetMonitoringReport is called on a charging station whenever a GetMonitoringReportRequest is received from the CSMS. + OnGetMonitoringReport(request *GetMonitoringReportRequest) (response *GetMonitoringReportResponse, err error) + // OnSetMonitoringBase is called on a charging station whenever a SetMonitoringBaseRequest is received from the CSMS. + OnSetMonitoringBase(request *SetMonitoringBaseRequest) (response *SetMonitoringBaseResponse, err error) + // OnSetMonitoringLevel is called on a charging station whenever a SetMonitoringLevelRequest is received from the CSMS. + OnSetMonitoringLevel(request *SetMonitoringLevelRequest) (response *SetMonitoringLevelResponse, err error) + // OnSetVariableMonitoring is called on a charging station whenever a SetVariableMonitoringRequest is received from the CSMS. + OnSetVariableMonitoring(request *SetVariableMonitoringRequest) (response *SetVariableMonitoringResponse, err error) +} + +const ProfileName = "Diagnostics" + +var Profile = ocpp.NewProfile( + ProfileName, + ClearVariableMonitoringFeature{}, + CustomerInformationFeature{}, + GetLogFeature{}, + GetMonitoringReportFeature{}, + LogStatusNotificationFeature{}, + NotifyCustomerInformationFeature{}, + NotifyEventFeature{}, + NotifyMonitoringReportFeature{}, + SetMonitoringBaseFeature{}, + SetMonitoringLevelFeature{}, + SetVariableMonitoringFeature{}, +) diff --git a/ocpp2.1/diagnostics/get_log.go b/ocpp2.1/diagnostics/get_log.go new file mode 100644 index 00000000..b89094b4 --- /dev/null +++ b/ocpp2.1/diagnostics/get_log.go @@ -0,0 +1,111 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Log (CSMS -> CS) -------------------- + +const GetLogFeatureName = "GetLog" + +// LogType represents the type of log file that the Charging Station should send. It is used in GetLogRequest. +type LogType string + +// LogStatus represents the status returned by a Charging Station in a GetLogResponse. +type LogStatus string + +const ( + LogTypeDiagnostics LogType = "DiagnosticsLog" // This contains the field definition of a diagnostics log file + LogTypeSecurity LogType = "SecurityLog" // Sent by the CSMS to the Charging Station to request that the Charging Station uploads the security log + LogTypeDataCollector LogType = "DataCollectorLog" // The log of sampled measurements from the DataCollector component. + LogStatusAccepted LogStatus = "Accepted" // Accepted this log upload. This does not mean the log file is uploaded is successfully, the Charging Station will now start the log file upload. + LogStatusRejected LogStatus = "Rejected" // Log update request rejected. + LogStatusAcceptedCanceled LogStatus = "AcceptedCanceled" // Accepted this log upload, but in doing this has canceled an ongoing log file upload. +) + +func isValidLogType(fl validator.FieldLevel) bool { + status := LogType(fl.Field().String()) + switch status { + case LogTypeDiagnostics, LogTypeSecurity, LogTypeDataCollector: + return true + default: + return false + } +} + +func isValidLogStatus(fl validator.FieldLevel) bool { + status := LogStatus(fl.Field().String()) + switch status { + case LogStatusAccepted, LogStatusRejected, LogStatusAcceptedCanceled: + return true + default: + return false + } +} + +// LogParameters specifies the requested log and the location to which the log should be sent. It is used in GetLogRequest. +type LogParameters struct { + RemoteLocation string `json:"remoteLocation" validate:"required,max=512,url"` + OldestTimestamp *types.DateTime `json:"oldestTimestamp,omitempty" validate:"omitempty"` + LatestTimestamp *types.DateTime `json:"latestTimestamp,omitempty" validate:"omitempty"` +} + +// The field definition of the GetLog request payload sent by the CSMS to the Charging Station. +type GetLogRequest struct { + LogType LogType `json:"logType" validate:"required,logType21"` + RequestID int `json:"requestId" validate:"gte=0"` + Retries *int `json:"retries,omitempty" validate:"omitempty,gte=0"` + RetryInterval *int `json:"retryInterval,omitempty" validate:"omitempty,gte=0"` + Log LogParameters `json:"log" validate:"required"` +} + +// This field definition of the GetLog response payload, sent by the Charging Station to the CSMS in response to a GetLogRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetLogResponse struct { + Status LogStatus `json:"status" validate:"required,logStatus21"` // This field indicates whether the Charging Station was able to accept the request. + Filename string `json:"filename,omitempty" validate:"omitempty,max=256"` // This contains the name of the log file that will be uploaded. This field is not present when no logging information is available. +} + +// The CSMS can request a Charging Station to upload a file with log information to a given location (URL). +// The format of this log file is not prescribed. +// The Charging Station responds with GetLogResponse. +// It then attempts to upload a log file asynchronously and gives information about the status of the upload by sending status notifications to the CSMS. +type GetLogFeature struct{} + +func (f GetLogFeature) GetFeatureName() string { + return GetLogFeatureName +} + +func (f GetLogFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetLogRequest{}) +} + +func (f GetLogFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetLogResponse{}) +} + +func (r GetLogRequest) GetFeatureName() string { + return GetLogFeatureName +} + +func (c GetLogResponse) GetFeatureName() string { + return GetLogFeatureName +} + +// Creates a new GetLogRequest, containing all required fields. Optional fields may be set afterwards. +func NewGetLogRequest(logType LogType, requestID int, logParameters LogParameters) *GetLogRequest { + return &GetLogRequest{LogType: logType, RequestID: requestID, Log: logParameters} +} + +// Creates a new GetLogResponse, containing all required fields. Optional fields may be set afterwards. +func NewGetLogResponse(status LogStatus) *GetLogResponse { + return &GetLogResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("logType21", isValidLogType) + _ = types.Validate.RegisterValidation("logStatus21", isValidLogStatus) +} diff --git a/ocpp2.1/diagnostics/get_monitoring_report.go b/ocpp2.1/diagnostics/get_monitoring_report.go new file mode 100644 index 00000000..43195fc9 --- /dev/null +++ b/ocpp2.1/diagnostics/get_monitoring_report.go @@ -0,0 +1,85 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Monitoring Report (CSMS -> CS) -------------------- + +const GetMonitoringReportFeatureName = "GetMonitoringReport" + +// Monitoring criteria contained in GetMonitoringReportRequest. +type MonitoringCriteriaType string + +const ( + MonitoringCriteriaThresholdMonitoring MonitoringCriteriaType = "ThresholdMonitoring" + MonitoringCriteriaDeltaMonitoring MonitoringCriteriaType = "DeltaMonitoring" + MonitoringCriteriaPeriodicMonitoring MonitoringCriteriaType = "PeriodicMonitoring" +) + +func isValidMonitoringCriteriaType(fl validator.FieldLevel) bool { + status := MonitoringCriteriaType(fl.Field().String()) + switch status { + case MonitoringCriteriaThresholdMonitoring, MonitoringCriteriaDeltaMonitoring, MonitoringCriteriaPeriodicMonitoring: + return true + default: + return false + } +} + +// The field definition of the GetMonitoringReport request payload sent by the CSMS to the Charging Station. +type GetMonitoringReportRequest struct { + RequestID *int `json:"requestId,omitempty" validate:"omitempty,gte=0"` // The Id of the request. + MonitoringCriteria []MonitoringCriteriaType `json:"monitoringCriteria,omitempty" validate:"omitempty,max=3,dive,monitoringCriteria21"` // This field contains criteria for components for which a monitoring report is requested. + ComponentVariable []types.ComponentVariable `json:"componentVariable,omitempty" validate:"omitempty,dive"` // This field specifies the components and variables for which a monitoring report is requested. +} + +// This field definition of the GetMonitoringReport response payload, sent by the Charging Station to the CSMS in response to a GetMonitoringReportRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetMonitoringReportResponse struct { + Status types.GenericDeviceModelStatus `json:"status" validate:"required,genericDeviceModelStatus21"` // This field indicates whether the Charging Station was able to accept the request. +} + +// A CSMS can request the Charging Station to send a report about configured monitoring settings per component and variable. +// Optionally, this list can be filtered on monitoringCriteria and componentVariables. +// The CSMS sends a GetMonitoringReportRequest to the Charging Station. +// The Charging Station then responds with a GetMonitoringReportResponse. +// Asynchronously, the Charging Station will then send a NotifyMonitoringReportRequest to the CSMS for each report part. +type GetMonitoringReportFeature struct{} + +func (f GetMonitoringReportFeature) GetFeatureName() string { + return GetMonitoringReportFeatureName +} + +func (f GetMonitoringReportFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetMonitoringReportRequest{}) +} + +func (f GetMonitoringReportFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetMonitoringReportResponse{}) +} + +func (r GetMonitoringReportRequest) GetFeatureName() string { + return GetMonitoringReportFeatureName +} + +func (c GetMonitoringReportResponse) GetFeatureName() string { + return GetMonitoringReportFeatureName +} + +// Creates a new GetMonitoringReportRequest. All fields are optional and may be set afterwards. +func NewGetMonitoringReportRequest() *GetMonitoringReportRequest { + return &GetMonitoringReportRequest{} +} + +// Creates a new GetMonitoringReportResponse, containing all required fields. There are no optional fields for this message. +func NewGetMonitoringReportResponse(status types.GenericDeviceModelStatus) *GetMonitoringReportResponse { + return &GetMonitoringReportResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("monitoringCriteria21", isValidMonitoringCriteriaType) +} diff --git a/ocpp2.1/diagnostics/log_status_notification.go b/ocpp2.1/diagnostics/log_status_notification.go new file mode 100644 index 00000000..987431ab --- /dev/null +++ b/ocpp2.1/diagnostics/log_status_notification.go @@ -0,0 +1,87 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Log Status Notification (CS -> CSMS) -------------------- + +const LogStatusNotificationFeatureName = "LogStatusNotification" + +// UploadLogStatus represents the current status of the log-upload procedure, reported by a Charging Station in a LogStatusNotificationRequest. +type UploadLogStatus string + +const ( + UploadLogStatusBadMessage UploadLogStatus = "BadMessage" // A badly formatted packet or other protocol incompatibility was detected. + UploadLogStatusIdle UploadLogStatus = "Idle" // The Charging Station is not uploading a log file. Idle SHALL only be used when the message was triggered by a TriggerMessageRequest. + UploadLogStatusNotSupportedOp UploadLogStatus = "NotSupportedOperation" // The server does not support the operation. + UploadLogStatusPermissionDenied UploadLogStatus = "PermissionDenied" // Insufficient permissions to perform the operation. + UploadLogStatusUploaded UploadLogStatus = "Uploaded" // File has been uploaded successfully. + UploadLogStatusUploadFailure UploadLogStatus = "UploadFailure" // Failed to upload the requested file. + UploadLogStatusUploading UploadLogStatus = "Uploading" // File is being uploaded. +) + +func isValidUploadLogStatus(fl validator.FieldLevel) bool { + status := UploadLogStatus(fl.Field().String()) + switch status { + case UploadLogStatusBadMessage, UploadLogStatusIdle, UploadLogStatusNotSupportedOp, UploadLogStatusPermissionDenied, UploadLogStatusUploaded, UploadLogStatusUploadFailure, UploadLogStatusUploading: + return true + default: + return false + } +} + +// The field definition of the LogStatusNotification request payload sent by a Charging Station to the CSMS. +type LogStatusNotificationRequest struct { + Status UploadLogStatus `json:"status" validate:"required,uploadLogStatus21"` + RequestID int `json:"requestId" validate:"gte=0"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +// This field definition of the LogStatusNotification response payload, sent by the CSMS to the Charging Station in response to a LogStatusNotificationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type LogStatusNotificationResponse struct { +} + +// A Charging Station shall send LogStatusNotification requests to update the CSMS with the current status of a log-upload procedure. +// The CSMS shall respond with a LogStatusNotificationResponse acknowledging the status update request. +// +// After a successful log upload, the The Charging Station returns to Idle status. +type LogStatusNotificationFeature struct{} + +func (f LogStatusNotificationFeature) GetFeatureName() string { + return LogStatusNotificationFeatureName +} + +func (f LogStatusNotificationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(LogStatusNotificationRequest{}) +} + +func (f LogStatusNotificationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(LogStatusNotificationResponse{}) +} + +func (r LogStatusNotificationRequest) GetFeatureName() string { + return LogStatusNotificationFeatureName +} + +func (c LogStatusNotificationResponse) GetFeatureName() string { + return LogStatusNotificationFeatureName +} + +// Creates a new LogStatusNotificationRequest, containing all required fields. There are no optional fields for this message. +func NewLogStatusNotificationRequest(status UploadLogStatus, requestID int) *LogStatusNotificationRequest { + return &LogStatusNotificationRequest{Status: status, RequestID: requestID} +} + +// Creates a new LogStatusNotificationResponse, which doesn't contain any required or optional fields. +func NewLogStatusNotificationResponse() *LogStatusNotificationResponse { + return &LogStatusNotificationResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("uploadLogStatus21", isValidUploadLogStatus) +} diff --git a/ocpp2.1/diagnostics/notify_customer_information.go b/ocpp2.1/diagnostics/notify_customer_information.go new file mode 100644 index 00000000..cc117636 --- /dev/null +++ b/ocpp2.1/diagnostics/notify_customer_information.go @@ -0,0 +1,62 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Notify Customer Information (CS -> CSMS) -------------------- + +const NotifyCustomerInformationFeatureName = "NotifyCustomerInformation" + +// The field definition of the NotifyCustomerInformation request payload sent by a Charging Station to the CSMS. +type NotifyCustomerInformationRequest struct { + Data string `json:"data" validate:"required,max=512"` // (Part of) the requested data. No format specified in which the data is returned. Should be human readable. + Tbc bool `json:"tbc,omitempty" validate:"omitempty"` // “to be continued” indicator. Indicates whether another part of the monitoringData follows in an upcoming notifyMonitoringReportRequest message. Default value when omitted is false. + SeqNo int `json:"seqNo" validate:"gte=0"` // Sequence number of this message. First message starts at 0. + GeneratedAt types.DateTime `json:"generatedAt" validate:"required"` // Timestamp of the moment this message was generated at the Charging Station. + RequestID int `json:"requestId" validate:"gte=0"` // The Id of the request. +} + +// This field definition of the NotifyCustomerInformation response payload, sent by the CSMS to the Charging Station in response to a NotifyCustomerInformationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyCustomerInformationResponse struct { +} + +// The CSMS may send a message to the Charging Station to retrieve raw customer information, for example to be compliant with local privacy laws. +// The Charging Station notifies the CSMS by sending one or more reports. +// For each report, the Charging station shall send a NotifyCustomerInformationRequest to the CSMS. +// +// The CSMS responds with a NotifyCustomerInformationResponse message to the Charging Station for each received NotifyCustomerInformationRequest. +type NotifyCustomerInformationFeature struct{} + +func (f NotifyCustomerInformationFeature) GetFeatureName() string { + return NotifyCustomerInformationFeatureName +} + +func (f NotifyCustomerInformationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyCustomerInformationRequest{}) +} + +func (f NotifyCustomerInformationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyCustomerInformationResponse{}) +} + +func (r NotifyCustomerInformationRequest) GetFeatureName() string { + return NotifyCustomerInformationFeatureName +} + +func (c NotifyCustomerInformationResponse) GetFeatureName() string { + return NotifyCustomerInformationFeatureName +} + +// Creates a new NotifyCustomerInformationRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyCustomerInformationRequest(Data string, seqNo int, generatedAt types.DateTime, requestID int) *NotifyCustomerInformationRequest { + return &NotifyCustomerInformationRequest{Data: Data, SeqNo: seqNo, GeneratedAt: generatedAt, RequestID: requestID} +} + +// Creates a new NotifyCustomerInformationResponse, which doesn't contain any required or optional fields. +func NewNotifyCustomerInformationResponse() *NotifyCustomerInformationResponse { + return &NotifyCustomerInformationResponse{} +} diff --git a/ocpp2.1/diagnostics/notify_event.go b/ocpp2.1/diagnostics/notify_event.go new file mode 100644 index 00000000..e783dd90 --- /dev/null +++ b/ocpp2.1/diagnostics/notify_event.go @@ -0,0 +1,127 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Notify Event (CS -> CSMS) -------------------- + +const NotifyEventFeatureName = "NotifyEvent" + +// EventTrigger defines the type of monitor that triggered an event. +type EventTrigger string + +const ( + EventTriggerAlerting EventTrigger = "Alerting" // Monitored variable has passed an Alert or Critical threshold. + EventTriggerDelta EventTrigger = "Delta" // Delta Monitored Variable value has changed by more than specified amount. + EventTriggerPeriodic EventTrigger = "Periodic" // Periodic Monitored Variable has been sampled for reporting at the specified interval. +) + +func isValidEventTrigger(fl validator.FieldLevel) bool { + status := EventTrigger(fl.Field().String()) + switch status { + case EventTriggerAlerting, EventTriggerDelta, EventTriggerPeriodic: + return true + default: + return false + } +} + +// EventNotification specifies the event notification type of the message. +type EventNotification string + +const ( + EventHardWiredNotification EventNotification = "HardWiredNotification" // The software implemented by the manufacturer triggered a hardwired notification. + EventHardWiredMonitor EventNotification = "HardWiredMonitor" // Triggered by a monitor, which is hardwired by the manufacturer. + EventPreconfiguredMonitor EventNotification = "PreconfiguredMonitor" // Triggered by a monitor, which is preconfigured by the manufacturer. + EventCustomMonitor EventNotification = "CustomMonitor" // Triggered by a monitor, which is set with the setvariablemonitoringrequest message by the Charging Station Operator. +) + +func isValidEventNotification(fl validator.FieldLevel) bool { + status := EventNotification(fl.Field().String()) + switch status { + case EventHardWiredMonitor, EventHardWiredNotification, EventPreconfiguredMonitor, EventCustomMonitor: + return true + default: + return false + } +} + +// An EventData element contains only the Component, Variable and VariableMonitoring data that caused an event. +type EventData struct { + EventID int `json:"eventId" validate:"gte=0"` + Timestamp *types.DateTime `json:"timestamp" validate:"required"` + Trigger EventTrigger `json:"trigger" validate:"required,eventTrigger21"` + Cause *int `json:"cause,omitempty" validate:"omitempty"` + ActualValue string `json:"actualValue" validate:"required,max=2500"` + TechCode string `json:"techCode,omitempty" validate:"omitempty,max=50"` + TechInfo string `json:"techInfo,omitempty" validate:"omitempty,max=500"` + Cleared bool `json:"cleared,omitempty"` + TransactionID string `json:"transactionId,omitempty" validate:"omitempty,max=36"` + VariableMonitoringID *int `json:"variableMonitoringId,omitempty" validate:"omitempty"` + EventNotificationType EventNotification `json:"eventNotificationType" validate:"required,eventNotification21"` + Severity *int `json:"severity,omitempty" validate:"omitempty"` + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` +} + +// The field definition of the NotifyEvent request payload sent by a Charging Station to the CSMS. +type NotifyEventRequest struct { + GeneratedAt *types.DateTime `json:"generatedAt" validate:"required"` // Timestamp of the moment this message was generated at the Charging Station. + SeqNo int `json:"seqNo" validate:"gte=0"` // Sequence number of this message. First message starts at 0. + Tbc bool `json:"tbc,omitempty" validate:"omitempty"` // “to be continued” indicator. Indicates whether another part of the monitoringData follows in an upcoming notifyMonitoringReportRequest message. Default value when omitted is false. + EventData []EventData `json:"eventData" validate:"required,min=1,dive"` // The list of EventData will usually contain one eventData element, but the Charging Station may decide to group multiple events in one notification. +} + +// This field definition of the NotifyEvent response payload, sent by the CSMS to the Charging Station in response to a NotifyEventRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyEventResponse struct { +} + +// The NotifyEvent feature gives Charging Stations the ability to notify the CSMS (periodically) about monitoring events. +// If a threshold or a delta value has exceeded, the Charging Station sends a NotifyEventRequest to the CSMS. +// A request reports every Component/Variable for which a VariableMonitoring setting was triggered. +// Only the VariableMonitoring settings that are responsible for triggering an event are included. +// The monitoring setting(s) might have been configured explicitly via a SetVariableMonitoring message or +// it might be "hard-wired" in the Charging Station’s firmware. +// +// The CSMS responds to the request with a NotifyEventResponse. +type NotifyEventFeature struct{} + +func (f NotifyEventFeature) GetFeatureName() string { + return NotifyEventFeatureName +} + +func (f NotifyEventFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyEventRequest{}) +} + +func (f NotifyEventFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyEventResponse{}) +} + +func (r NotifyEventRequest) GetFeatureName() string { + return NotifyEventFeatureName +} + +func (c NotifyEventResponse) GetFeatureName() string { + return NotifyEventFeatureName +} + +// Creates a new NotifyEventRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyEventRequest(generatedAt *types.DateTime, seqNo int, eventData []EventData) *NotifyEventRequest { + return &NotifyEventRequest{GeneratedAt: generatedAt, SeqNo: seqNo, EventData: eventData} +} + +// Creates a new NotifyEventResponse, which doesn't contain any required or optional fields. +func NewNotifyEventResponse() *NotifyEventResponse { + return &NotifyEventResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("eventTrigger21", isValidEventTrigger) + _ = types.Validate.RegisterValidation("eventNotification21", isValidEventNotification) +} diff --git a/ocpp2.1/diagnostics/notify_monitoring_report.go b/ocpp2.1/diagnostics/notify_monitoring_report.go new file mode 100644 index 00000000..e9be05e6 --- /dev/null +++ b/ocpp2.1/diagnostics/notify_monitoring_report.go @@ -0,0 +1,85 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Notify Monitoring Report (CS -> CSMS) -------------------- + +const NotifyMonitoringReportFeatureName = "NotifyMonitoringReport" + +// VariableMonitoring describes a monitoring setting for a variable. +type VariableMonitoring struct { + ID int `json:"id" validate:"gte=0"` // Identifies the monitor. + Transaction bool `json:"transaction"` // Monitor only active when a transaction is ongoing on a component relevant to this transaction. + Value float64 `json:"value"` // Value for threshold or delta monitoring. For Periodic or PeriodicClockAligned this is the interval in seconds. + Type MonitorType `json:"type" validate:"required,monitorType"` // The type of this monitor, e.g. a threshold, delta or periodic monitor. + Severity int `json:"severity" validate:"min=0,max=9"` // The severity that will be assigned to an event that is triggered by this monitor. The severity range is 0-9, with 0 as the highest and 9 as the lowest severity level. +} + +// NewVariableMonitoring is a utility function for creating a VariableMonitoring struct. +func NewVariableMonitoring(id int, transaction bool, value float64, t MonitorType, severity int) VariableMonitoring { + return VariableMonitoring{ID: id, Transaction: transaction, Value: value, Type: t, Severity: severity} +} + +// MonitoringData holds parameters of SetVariableMonitoring request. +type MonitoringData struct { + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` + VariableMonitoring []VariableMonitoring `json:"variableMonitoring" validate:"required,min=1,dive"` +} + +// The field definition of the NotifyMonitoringReport request payload sent by a Charging Station to the CSMS. +type NotifyMonitoringReportRequest struct { + RequestID int `json:"requestId" validate:"gte=0"` // The id of the GetMonitoringRequest that requested this report. + Tbc bool `json:"tbc,omitempty" validate:"omitempty"` // “to be continued” indicator. Indicates whether another part of the monitoringData follows in an upcoming notifyMonitoringReportRequest message. Default value when omitted is false. + SeqNo int `json:"seqNo" validate:"gte=0"` // Sequence number of this message. First message starts at 0. + GeneratedAt *types.DateTime `json:"generatedAt" validate:"required"` // Timestamp of the moment this message was generated at the Charging Station. + Monitor []MonitoringData `json:"monitor,omitempty" validate:"omitempty,dive"` // List of MonitoringData containing monitoring settings. +} + +// This field definition of the NotifyMonitoringReport response payload, sent by the CSMS to the Charging Station in response to a NotifyMonitoringReportRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyMonitoringReportResponse struct { +} + +// The NotifyMonitoringReport feature is used by a Charging Station to send a report to the CSMS about configured +// monitoring settings per component and variable. +// Optionally, this list can be filtered on monitoringCriteria and componentVariables. +// After responding to a GetMonitoringReportRequest, a Charging Station will send one or more +// NotifyMonitoringReportRequest asynchronously to the CSMS, until all data of the monitoring report has been sent. +// +// The CSMS responds with a NotifyMonitoringReportResponse for every received received request. +type NotifyMonitoringReportFeature struct{} + +func (f NotifyMonitoringReportFeature) GetFeatureName() string { + return NotifyMonitoringReportFeatureName +} + +func (f NotifyMonitoringReportFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyMonitoringReportRequest{}) +} + +func (f NotifyMonitoringReportFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyMonitoringReportResponse{}) +} + +func (r NotifyMonitoringReportRequest) GetFeatureName() string { + return NotifyMonitoringReportFeatureName +} + +func (c NotifyMonitoringReportResponse) GetFeatureName() string { + return NotifyMonitoringReportFeatureName +} + +// Creates a new NotifyMonitoringReportRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyMonitoringReportRequest(requestID int, seqNo int, generatedAt *types.DateTime, monitorData []MonitoringData) *NotifyMonitoringReportRequest { + return &NotifyMonitoringReportRequest{RequestID: requestID, SeqNo: seqNo, GeneratedAt: generatedAt, Monitor: monitorData} +} + +// Creates a new NotifyMonitoringReportResponse, which doesn't contain any required or optional fields. +func NewNotifyMonitoringReportResponse() *NotifyMonitoringReportResponse { + return &NotifyMonitoringReportResponse{} +} diff --git a/ocpp2.1/diagnostics/set_monitoring_base.go b/ocpp2.1/diagnostics/set_monitoring_base.go new file mode 100644 index 00000000..6efe032c --- /dev/null +++ b/ocpp2.1/diagnostics/set_monitoring_base.go @@ -0,0 +1,86 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Set Monitoring Base (CSMS -> CS) -------------------- + +const SetMonitoringBaseFeatureName = "SetMonitoringBase" + +// Monitoring base to be set within the Charging Station. +type MonitoringBase string + +const ( + MonitoringBaseAll MonitoringBase = "All" + MonitoringBaseFactoryDefault MonitoringBase = "FactoryDefault" + MonitoringBaseHardWiredOnly MonitoringBase = "HardWiredOnly" +) + +func isValidMonitoringBase(fl validator.FieldLevel) bool { + status := MonitoringBase(fl.Field().String()) + switch status { + case MonitoringBaseAll, MonitoringBaseFactoryDefault, MonitoringBaseHardWiredOnly: + return true + default: + return false + } +} + +// The field definition of the SetMonitoringBase request payload sent by the CSMS to the Charging Station. +type SetMonitoringBaseRequest struct { + MonitoringBase MonitoringBase `json:"monitoringBase" validate:"required,monitoringBase21"` // Specifies which monitoring base will be set. +} + +// This field definition of the SetMonitoringBase response payload, sent by the Charging Station to the CSMS in response to a SetMonitoringBaseRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetMonitoringBaseResponse struct { + Status types.GenericDeviceModelStatus `json:"status" validate:"required,genericDeviceModelStatus21"` // Indicates whether the Charging Station was able to accept the request. + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` // Detailed status information. +} + +// A CSMS has the ability to request the Charging Station to activate a set of preconfigured +// monitoring settings, as denoted by the value of MonitoringBase. This is achieved by sending a +// SetMonitoringBaseRequest to the charging station. The charging station will respond with a +// SetMonitoringBaseResponse message. +// +// It is up to the manufacturer of the Charging Station to define which monitoring settings are activated +// by All, FactoryDefault and HardWiredOnly. +type SetMonitoringBaseFeature struct{} + +func (f SetMonitoringBaseFeature) GetFeatureName() string { + return SetMonitoringBaseFeatureName +} + +func (f SetMonitoringBaseFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetMonitoringBaseRequest{}) +} + +func (f SetMonitoringBaseFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetMonitoringBaseResponse{}) +} + +func (r SetMonitoringBaseRequest) GetFeatureName() string { + return SetMonitoringBaseFeatureName +} + +func (c SetMonitoringBaseResponse) GetFeatureName() string { + return SetMonitoringBaseFeatureName +} + +// Creates a new SetMonitoringBaseRequest, containing all required fields. There are no optional fields for this message. +func NewSetMonitoringBaseRequest(monitoringBase MonitoringBase) *SetMonitoringBaseRequest { + return &SetMonitoringBaseRequest{MonitoringBase: monitoringBase} +} + +// Creates a new SetMonitoringBaseResponse, containing all required fields. Optional fields may be set afterwards. +func NewSetMonitoringBaseResponse(status types.GenericDeviceModelStatus) *SetMonitoringBaseResponse { + return &SetMonitoringBaseResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("monitoringBase21", isValidMonitoringBase) +} diff --git a/ocpp2.1/diagnostics/set_monitoring_level.go b/ocpp2.1/diagnostics/set_monitoring_level.go new file mode 100644 index 00000000..9f379945 --- /dev/null +++ b/ocpp2.1/diagnostics/set_monitoring_level.go @@ -0,0 +1,91 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Set Monitoring Level (CSMS -> CS) -------------------- + +const SetMonitoringLevelFeatureName = "SetMonitoringLevel" + +// The field definition of the SetMonitoringLevel request payload sent by the CSMS to the Charging Station. +type SetMonitoringLevelRequest struct { + // Severity levels have the following meaning: + // + // - 0 Danger: + // Indicates lives are potentially in danger. Urgent attention + // is needed and action should be taken immediately. + // - 1 Hardware Failure: + // Indicates that the Charging Station is unable to continue regular operations due to Hardware issues. + // - 2 System Failure: + // Indicates that the Charging Station is unable to continue regular operations due to software or minor hardware + // issues. + // - 3 Critical: + // Indicates a critical error. + // - 4 Error: + // Indicates a non-urgent error. + // - 5 Alert: + // Indicates an alert event. Default severity for any type of monitoring event. + // - 6 Warning: + // Indicates a warning event. + // - 7 Notice: + // Indicates an unusual event. + // - 8 Informational: + // Indicates a regular operational event. May be used for reporting, measuring throughput, etc. + // - 9 Debug: + // Indicates information useful to developers for debugging, not useful during operations. + Severity int `json:"severity" validate:"min=0,max=9"` +} + +// This field definition of the SetMonitoringLevel response payload, sent by the Charging Station to the CSMS in response to a SetMonitoringLevelRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetMonitoringLevelResponse struct { + Status types.GenericDeviceModelStatus `json:"status" validate:"required,genericDeviceModelStatus21"` // Indicates whether the Charging Station was able to accept the request. + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` // Detailed status information. +} + +// It may be desirable to restrict the reporting of monitoring events, to only those monitors with a +// severity number lower than or equal to a certain severity. For example when the data-traffic between +// Charging Station and CSMS needs to limited for some reason. +// +// The CSMS can control which events it will to be notified of by the Charging Station with the +// SetMonitoringLevelRequest message. The charging station responds with a SetMonitoringLevelResponse. +// Monitoring events, reported later on via NotifyEventRequest messages, +// will be restricted according to the set monitoring level. +type SetMonitoringLevelFeature struct{} + +func (f SetMonitoringLevelFeature) GetFeatureName() string { + return SetMonitoringLevelFeatureName +} + +func (f SetMonitoringLevelFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetMonitoringLevelRequest{}) +} + +func (f SetMonitoringLevelFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetMonitoringLevelResponse{}) +} + +func (r SetMonitoringLevelRequest) GetFeatureName() string { + return SetMonitoringLevelFeatureName +} + +func (c SetMonitoringLevelResponse) GetFeatureName() string { + return SetMonitoringLevelFeatureName +} + +// Creates a new SetMonitoringLevelRequest, containing all required fields. There are no optional fields for this message. +func NewSetMonitoringLevelRequest(severity int) *SetMonitoringLevelRequest { + return &SetMonitoringLevelRequest{Severity: severity} +} + +// Creates a new SetMonitoringLevelResponse, containing all required fields. Optional fields may be set afterwards. +func NewSetMonitoringLevelResponse(status types.GenericDeviceModelStatus) *SetMonitoringLevelResponse { + return &SetMonitoringLevelResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("monitoringBase21", isValidMonitoringBase) +} diff --git a/ocpp2.1/diagnostics/set_variable_monitoring.go b/ocpp2.1/diagnostics/set_variable_monitoring.go new file mode 100644 index 00000000..0c5d9d7a --- /dev/null +++ b/ocpp2.1/diagnostics/set_variable_monitoring.go @@ -0,0 +1,114 @@ +package diagnostics + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Set Variable Monitoring (CSMS -> CS) -------------------- + +const SetVariableMonitoringFeatureName = "SetVariableMonitoring" + +// Status contained inside a SetMonitoringResult struct. +type SetMonitoringStatus string + +const ( + SetMonitoringStatusAccepted SetMonitoringStatus = "Accepted" + SetMonitoringStatusUnknownComponent SetMonitoringStatus = "UnknownComponent" + SetMonitoringStatusUnknownVariable SetMonitoringStatus = "UnknownVariable" + SetMonitoringStatusUnsupportedMonitorType SetMonitoringStatus = "UnsupportedMonitorType" + SetMonitoringStatusRejected SetMonitoringStatus = "Rejected" + SetMonitoringStatusDuplicate SetMonitoringStatus = "Duplicate" +) + +func isValidSetMonitoringStatus(fl validator.FieldLevel) bool { + status := SetMonitoringStatus(fl.Field().String()) + switch status { + case SetMonitoringStatusAccepted, SetMonitoringStatusUnknownComponent, SetMonitoringStatusUnknownVariable, SetMonitoringStatusUnsupportedMonitorType, SetMonitoringStatusRejected, SetMonitoringStatusDuplicate: + return true + default: + return false + } +} + +// Hold parameters of a SetVariableMonitoring request. +type SetMonitoringData struct { + ID *int `json:"id,omitempty" validate:"omitempty"` // An id SHALL only be given to replace an existing monitor. The Charging Station handles the generation of id’s for new monitors. + Transaction bool `json:"transaction,omitempty"` // Monitor only active when a transaction is ongoing on a component relevant to this transaction. + Value float64 `json:"value"` // Value for threshold or delta monitoring. For Periodic or PeriodicClockAligned this is the interval in seconds. + Type MonitorType `json:"type" validate:"required,monitorType"` // The type of this monitor, e.g. a threshold, delta or periodic monitor. + Severity int `json:"severity" validate:"min=0,max=9"` // The severity that will be assigned to an event that is triggered by this monitor. The severity range is 0-9, with 0 as the highest and 9 as the lowest severity level. + Component types.Component `json:"component" validate:"required"` // Component for which monitor is set. + Variable types.Variable `json:"variable" validate:"required"` // Variable for which monitor is set. + PeriodicEventStream PeriodicEventStreamParams `json:"periodicEventStream,omitempty" validate:"omitempty,dive"` +} + +type PeriodicEventStreamParams struct { + Interval *int `json:"interval,omitempty" validate:"omitempty,gte=0"` // Interval in seconds for periodic monitoring. + ClockAligned *int `json:"clockAligned,omitempty" validate:"omitempty,gte=0"` +} + +// Holds the result of SetVariableMonitoring request. +type SetMonitoringResult struct { + ID *int `json:"id,omitempty" validate:"omitempty"` // Id given to the VariableMonitor by the Charging Station. The Id is only returned when status is accepted. + Status SetMonitoringStatus `json:"status" validate:"required,setMonitoringStatus21"` // Status is OK if a value could be returned. Otherwise this will indicate the reason why a value could not be returned. + Type MonitorType `json:"type" validate:"required,monitorType"` // The type of this monitor, e.g. a threshold, delta or periodic monitor. + Severity int `json:"severity" validate:"min=0,max=9"` // The severity that will be assigned to an event that is triggered by this monitor. The severity range is 0-9, with 0 as the highest and 9 as the lowest severity level. + Component types.Component `json:"component" validate:"required"` // Component for which status is returned. + Variable types.Variable `json:"variable" validate:"required"` // Variable for which status is returned. + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` // Detailed status information. +} + +// The field definition of the SetVariableMonitoring request payload sent by the CSMS to the Charging Station. +type SetVariableMonitoringRequest struct { + MonitoringData []SetMonitoringData `json:"setMonitoringData" validate:"required,min=1,dive"` // List of MonitoringData containing monitoring settings. +} + +// This field definition of the SetVariableMonitoring response payload, sent by the Charging Station to the CSMS in response to a SetVariableMonitoringRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetVariableMonitoringResponse struct { + MonitoringResult []SetMonitoringResult `json:"setMonitoringResult" validate:"required,min=1,dive"` // List of result statuses per monitor. +} + +// The CSMS may request the Charging Station to set monitoring triggers on Variables. Multiple triggers can be +// set for upper or lower thresholds, delta changes or periodic reporting. +// +// To achieve this, the CSMS sends a SetVariableMonitoringRequest to the Charging Station. +// The Charging Station responds with a SetVariableMonitoringResponse. +type SetVariableMonitoringFeature struct{} + +func (f SetVariableMonitoringFeature) GetFeatureName() string { + return SetVariableMonitoringFeatureName +} + +func (f SetVariableMonitoringFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetVariableMonitoringRequest{}) +} + +func (f SetVariableMonitoringFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetVariableMonitoringResponse{}) +} + +func (r SetVariableMonitoringRequest) GetFeatureName() string { + return SetVariableMonitoringFeatureName +} + +func (c SetVariableMonitoringResponse) GetFeatureName() string { + return SetVariableMonitoringFeatureName +} + +// Creates a new SetVariableMonitoringRequest, containing all required fields. There are no optional fields for this message. +func NewSetVariableMonitoringRequest(data []SetMonitoringData) *SetVariableMonitoringRequest { + return &SetVariableMonitoringRequest{MonitoringData: data} +} + +// Creates a new SetVariableMonitoringResponse, containing all required fields. There are no optional fields for this message. +func NewSetVariableMonitoringResponse(result []SetMonitoringResult) *SetVariableMonitoringResponse { + return &SetVariableMonitoringResponse{MonitoringResult: result} +} + +func init() { + _ = types.Validate.RegisterValidation("setMonitoringStatus21", isValidSetMonitoringStatus) +} diff --git a/ocpp2.1/diagnostics/types.go b/ocpp2.1/diagnostics/types.go new file mode 100644 index 00000000..b6ff2ecd --- /dev/null +++ b/ocpp2.1/diagnostics/types.go @@ -0,0 +1,34 @@ +package diagnostics + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// MonitorType specifies the type of this monitor. +type MonitorType string + +const ( + MonitorUpperThreshold MonitorType = "UpperThreshold" // Triggers an event notice when the actual value of the Variable rises above monitorValue. + MonitorLowerThreshold MonitorType = "LowerThreshold" // Triggers an event notice when the actual value of the Variable drops below monitorValue. + MonitorDelta MonitorType = "Delta" // Triggers an event notice when the actual value has changed more than plus or minus monitorValue since the time that this monitor was set or since the last time this event notice was sent, whichever was last. + MonitorPeriodic MonitorType = "Periodic" // Triggers an event notice every monitorValue seconds interval, starting from the time that this monitor was set. + MonitorPeriodicClockAligned MonitorType = "PeriodicClockAligned" // Triggers an event notice every monitorValue seconds interval, starting from the nearest clock-aligned interval after this monitor was set. + MonitorTargetDelta MonitorType = "TargetDelta" // Triggers an event notice when the actual value differs from the target value more than plus or minus value since the time that this monitor was set or since the last time this event notice was sent, whichever was last. + MonitorTargetDeltaRelative MonitorType = "TargetDeltaRelative" // Triggers an event notice when the actual value differs from the target value more than plus or minus (value * target value) since the time that this monitor was set or since the last time this event notice was sent, whichever was last. +) + +func isValidMonitorType(fl validator.FieldLevel) bool { + status := MonitorType(fl.Field().String()) + switch status { + case MonitorUpperThreshold, MonitorLowerThreshold, MonitorDelta, MonitorPeriodic, MonitorPeriodicClockAligned, + MonitorTargetDelta, MonitorTargetDeltaRelative: + return true + default: + return false + } +} + +func init() { + _ = types.Validate.RegisterValidation("monitorType21", isValidMonitorType) +} diff --git a/ocpp2.1/display/clear_display_message.go b/ocpp2.1/display/clear_display_message.go new file mode 100644 index 00000000..349e8eb1 --- /dev/null +++ b/ocpp2.1/display/clear_display_message.go @@ -0,0 +1,82 @@ +package display + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Clear Display Message (CSMS -> CS) -------------------- + +const ClearDisplayMessageFeatureName = "ClearDisplayMessage" + +// Status returned in response to ClearDisplayRequest. +type ClearMessageStatus string + +const ( + ClearMessageStatusAccepted ClearMessageStatus = "Accepted" + ClearMessageStatusUnknown ClearMessageStatus = "Unknown" +) + +func isValidClearMessageStatus(fl validator.FieldLevel) bool { + status := ClearMessageStatus(fl.Field().String()) + switch status { + case ClearMessageStatusAccepted, ClearMessageStatusUnknown: + return true + default: + return false + } +} + +// The field definition of the ClearDisplay request payload sent by the CSMS to the Charging Station. +type ClearDisplayRequest struct { + ID int `json:"id"` // Id of the message that SHALL be removed from the Charging Station. +} + +// This field definition of the ClearDisplay response payload, sent by the Charging Station to the CSMS in response to a ClearDisplayRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClearDisplayResponse struct { + Status ClearMessageStatus `json:"status" validate:"required,clearMessageStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS asks the Charging Station to clear a display message that has been configured in the Charging Station to be cleared/removed. +// The Charging station checks for a message with the requested ID and removes it. +// The Charging station then responds with a ClearDisplayResponse. The response payload indicates whether the Charging Station was able to remove the message from display or not. +type ClearDisplayFeature struct{} + +func (f ClearDisplayFeature) GetFeatureName() string { + return ClearDisplayMessageFeatureName +} + +func (f ClearDisplayFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearDisplayRequest{}) +} + +func (f ClearDisplayFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearDisplayResponse{}) +} + +func (r ClearDisplayRequest) GetFeatureName() string { + return ClearDisplayMessageFeatureName +} + +func (c ClearDisplayResponse) GetFeatureName() string { + return ClearDisplayMessageFeatureName +} + +// Creates a new ClearDisplayRequest, containing all required fields. There are no optional fields for this message. +func NewClearDisplayRequest(id int) *ClearDisplayRequest { + return &ClearDisplayRequest{ID: id} +} + +// Creates a new ClearDisplayResponse, containing all required fields. Optional fields may be set afterwards. +func NewClearDisplayResponse(status ClearMessageStatus) *ClearDisplayResponse { + return &ClearDisplayResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("clearMessageStatus21", isValidClearMessageStatus) +} diff --git a/ocpp2.1/display/display.go b/ocpp2.1/display/display.go new file mode 100644 index 00000000..33b28fe3 --- /dev/null +++ b/ocpp2.1/display/display.go @@ -0,0 +1,30 @@ +// The display functional block contains OCPP 2.1 features for managing message that get displayed on a charging station. +package display + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Display profile. +type CSMSHandler interface { + // OnNotifyDisplayMessages is called on the CSMS whenever a NotifyDisplayMessagesRequest is received from a Charging Station. + OnNotifyDisplayMessages(chargingStationID string, request *NotifyDisplayMessagesRequest) (response *NotifyDisplayMessagesResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Display profile. +type ChargingStationHandler interface { + // OnClearDisplay is called on a charging station whenever a ClearDisplayRequest is received from the CSMS. + OnClearDisplay(request *ClearDisplayRequest) (confirmation *ClearDisplayResponse, err error) + // OnGetDisplayMessages is called on a charging station whenever a GetDisplayMessagesRequest is received from the CSMS. + OnGetDisplayMessages(request *GetDisplayMessagesRequest) (confirmation *GetDisplayMessagesResponse, err error) + // OnSetDisplayMessage is called on a charging station whenever a SetDisplayMessageRequest is received from the CSMS. + OnSetDisplayMessage(request *SetDisplayMessageRequest) (response *SetDisplayMessageResponse, err error) +} + +const ProfileName = "Display" + +var Profile = ocpp.NewProfile( + ProfileName, + ClearDisplayFeature{}, + GetDisplayMessagesFeature{}, + NotifyDisplayMessagesFeature{}, + SetDisplayMessageFeature{}, +) diff --git a/ocpp2.1/display/get_display_messages.go b/ocpp2.1/display/get_display_messages.go new file mode 100644 index 00000000..d5c823bf --- /dev/null +++ b/ocpp2.1/display/get_display_messages.go @@ -0,0 +1,63 @@ +package display + +import ( + "reflect" +) + +// -------------------- Get Display Messages (CSMS -> CS) -------------------- + +const GetDisplayMessagesFeatureName = "GetDisplayMessages" + +// The field definition of the GetDisplayMessages request payload sent by the CSMS to the Charging Station. +type GetDisplayMessagesRequest struct { + RequestID int `json:"requestId" validate:"gte=0"` + Priority MessagePriority `json:"priority,omitempty" validate:"omitempty,messagePriority21"` + State MessageState `json:"state,omitempty" validate:"omitempty,messageState21"` + ID []int `json:"id,omitempty" validate:"omitempty,dive,gte=0"` +} + +// This field definition of the GetDisplayMessages response payload, sent by the Charging Station to the CSMS in response to a GetDisplayMessagesRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetDisplayMessagesResponse struct { + Status MessageStatus `json:"status" validate:"required,messageStatus21"` +} + +// A Charging Station can remove messages when they are out-dated, or transactions have ended. It can be very useful for a CSO to be able to view to current list of messages, so the CSO knows which messages are (still) configured. +// +// A CSO MAY request all the installed DisplayMessages configured via OCPP in a Charging Station. For this the CSO asks the CSMS to retrieve all messages. +// The CSMS sends a GetDisplayMessagesRequest message to the Charging Station. +// The Charging Station responds with a GetDisplayMessagesResponse Accepted, indicating it has configured messages and will send them. +// +// The Charging Station asynchronously sends one or more NotifyDisplayMessagesRequest messages to the +// CSMS (depending on the amount of messages to be sent). +type GetDisplayMessagesFeature struct{} + +func (f GetDisplayMessagesFeature) GetFeatureName() string { + return GetDisplayMessagesFeatureName +} + +func (f GetDisplayMessagesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetDisplayMessagesRequest{}) +} + +func (f GetDisplayMessagesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetDisplayMessagesResponse{}) +} + +func (r GetDisplayMessagesRequest) GetFeatureName() string { + return GetDisplayMessagesFeatureName +} + +func (c GetDisplayMessagesResponse) GetFeatureName() string { + return GetDisplayMessagesFeatureName +} + +// Creates a new GetDisplayMessagesRequest, containing all required fields. Optional fields may be set afterwards. +func NewGetDisplayMessagesRequest(requestId int) *GetDisplayMessagesRequest { + return &GetDisplayMessagesRequest{RequestID: requestId} +} + +// Creates a new GetDisplayMessagesResponse, containing all required fields. There are no optional fields for this message. +func NewGetDisplayMessagesResponse(status MessageStatus) *GetDisplayMessagesResponse { + return &GetDisplayMessagesResponse{Status: status} +} diff --git a/ocpp2.1/display/notify_display_messages.go b/ocpp2.1/display/notify_display_messages.go new file mode 100644 index 00000000..e19e1569 --- /dev/null +++ b/ocpp2.1/display/notify_display_messages.go @@ -0,0 +1,58 @@ +package display + +import ( + "reflect" +) + +// -------------------- Notify Display Messages (CS -> CSMS) -------------------- + +const NotifyDisplayMessagesFeatureName = "NotifyDisplayMessages" + +// The field definition of the NotifyDisplayMessages request payload sent by the CSMS to the Charging Station. +type NotifyDisplayMessagesRequest struct { + RequestID int `json:"requestId" validate:"gte=0"` // The id of the GetDisplayMessagesRequest that requested this message. + Tbc bool `json:"tbc,omitempty" validate:"omitempty"` // "to be continued" indicator. Indicates whether another part of the report follows in an upcoming NotifyDisplayMessagesRequest message. Default value when omitted is false. + MessageInfo []MessageInfo `json:"messageInfo,omitempty" validate:"omitempty,dive"` // The requested display message as configured in the Charging Station. +} + +// This field definition of the NotifyDisplayMessages response payload, sent by the Charging Station to the CSMS in response to a NotifyDisplayMessagesRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyDisplayMessagesResponse struct { +} + +// A CSO MAY request all the installed DisplayMessages configured via OCPP in a Charging Station. For this the CSO asks the CSMS to retrieve all messages (see GetDisplayMessagesFeature). +// If the Charging Station responded with a status Accepted, it will then send these messages asynchronously to the CSMS. +// +// The Charging Station sends one or more NotifyDisplayMessagesRequest message to the CSMS (depending on the amount of messages to be send). +// The CSMS responds to every notification with a NotifyDisplayMessagesResponse message. +type NotifyDisplayMessagesFeature struct{} + +func (f NotifyDisplayMessagesFeature) GetFeatureName() string { + return NotifyDisplayMessagesFeatureName +} + +func (f NotifyDisplayMessagesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyDisplayMessagesRequest{}) +} + +func (f NotifyDisplayMessagesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyDisplayMessagesResponse{}) +} + +func (r NotifyDisplayMessagesRequest) GetFeatureName() string { + return NotifyDisplayMessagesFeatureName +} + +func (c NotifyDisplayMessagesResponse) GetFeatureName() string { + return NotifyDisplayMessagesFeatureName +} + +// Creates a new NotifyDisplayMessagesRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyDisplayMessagesRequest(requestID int) *NotifyDisplayMessagesRequest { + return &NotifyDisplayMessagesRequest{RequestID: requestID} +} + +// Creates a new NotifyDisplayMessagesResponse, which doesn't contain any required or optional fields. +func NewNotifyDisplayMessagesResponse() *NotifyDisplayMessagesResponse { + return &NotifyDisplayMessagesResponse{} +} diff --git a/ocpp2.1/display/set_display_message.go b/ocpp2.1/display/set_display_message.go new file mode 100644 index 00000000..c23931cb --- /dev/null +++ b/ocpp2.1/display/set_display_message.go @@ -0,0 +1,92 @@ +package display + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Clear Display (CSMS -> CS) -------------------- + +const SetDisplayMessageFeatureName = "SetDisplayMessage" + +// Status returned in response to SetDisplayMessageRequest. +type DisplayMessageStatus string + +const ( + DisplayMessageStatusAccepted DisplayMessageStatus = "Accepted" + DisplayMessageStatusNotSupportedMessageFormat DisplayMessageStatus = "NotSupportedMessageFormat" + DisplayMessageStatusRejected DisplayMessageStatus = "Rejected" + DisplayMessageStatusNotSupportedPriority DisplayMessageStatus = "NotSupportedPriority" + DisplayMessageStatusNotSupportedState DisplayMessageStatus = "NotSupportedState" + DisplayMessageStatusUnknownTransaction DisplayMessageStatus = "UnknownTransaction" +) + +func isValidDisplayMessageStatus(fl validator.FieldLevel) bool { + status := DisplayMessageStatus(fl.Field().String()) + switch status { + case DisplayMessageStatusAccepted, + DisplayMessageStatusNotSupportedMessageFormat, + DisplayMessageStatusRejected, + DisplayMessageStatusNotSupportedPriority, + DisplayMessageStatusNotSupportedState, + DisplayMessageStatusUnknownTransaction: + return true + default: + return false + } +} + +// The field definition of the SetDisplayMessage request payload sent by the CSMS to the Charging Station. +type SetDisplayMessageRequest struct { + Message MessageInfo `json:"message" validate:"required"` +} + +// This field definition of the SetDisplayMessage response payload, sent by the Charging Station to the CSMS in response to a SetDisplayMessageRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetDisplayMessageResponse struct { + Status DisplayMessageStatus `json:"status" validate:"required,displayMessageStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS may send a SetDisplayMessageRequest message to a Charging Station, instructing it to display a new message, +// which is not part of its firmware. +// The Charging Station accepts the request by replying with a SetDisplayMessageResponse. +// +// Depending on different parameters, the message may be displayed in different ways and/or at a configured time. +type SetDisplayMessageFeature struct{} + +func (f SetDisplayMessageFeature) GetFeatureName() string { + return SetDisplayMessageFeatureName +} + +func (f SetDisplayMessageFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetDisplayMessageRequest{}) +} + +func (f SetDisplayMessageFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetDisplayMessageResponse{}) +} + +func (r SetDisplayMessageRequest) GetFeatureName() string { + return SetDisplayMessageFeatureName +} + +func (c SetDisplayMessageResponse) GetFeatureName() string { + return SetDisplayMessageFeatureName +} + +// Creates a new SetDisplayMessageRequest, containing all required fields. There are no optional fields for this message. +func NewSetDisplayMessageRequest(message MessageInfo) *SetDisplayMessageRequest { + return &SetDisplayMessageRequest{Message: message} +} + +// Creates a new SetDisplayMessageResponse, containing all required fields. Optional fields may be set afterwards. +func NewSetDisplayMessageResponse(status DisplayMessageStatus) *SetDisplayMessageResponse { + return &SetDisplayMessageResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("displayMessageStatus21", isValidDisplayMessageStatus) +} diff --git a/ocpp2.1/display/types.go b/ocpp2.1/display/types.go new file mode 100644 index 00000000..2a0185bc --- /dev/null +++ b/ocpp2.1/display/types.go @@ -0,0 +1,83 @@ +package display + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// Priority with which a message should be displayed on a Charging Station. +// Used within a GetDisplayMessagesRequest. +type MessagePriority string + +// State of the Charging Station during which a message SHALL be displayed. +// Used within a GetDisplayMessagesRequest. +type MessageState string + +// MessageStatus represents the status of the request, used in a GetDisplayMessagesResponse. +type MessageStatus string + +const ( + MessagePriorityAlwaysFront MessagePriority = "AlwaysFront" + MessagePriorityInFront MessagePriority = "InFront" + MessagePriorityNormalCycle MessagePriority = "NormalCycle" + + MessageStateCharging MessageState = "Charging" + MessageStateFaulted MessageState = "Faulted" + MessageStateIdle MessageState = "Idle" + MessageStateUnavailable MessageState = "Unavailable" + MessageStateSuspended MessageState = "Suspended" + MessageStateDischarging MessageState = "Discharging" + + MessageStatusAccepted MessageStatus = "Accepted" + MessageStatusUnknown MessageStatus = "Unknown" +) + +func isValidMessagePriority(fl validator.FieldLevel) bool { + priority := MessagePriority(fl.Field().String()) + switch priority { + case MessagePriorityAlwaysFront, MessagePriorityInFront, MessagePriorityNormalCycle: + return true + default: + return false + } +} + +func isValidMessageState(fl validator.FieldLevel) bool { + priority := MessageState(fl.Field().String()) + switch priority { + case MessageStateCharging, MessageStateFaulted, MessageStateIdle, MessageStateUnavailable, + MessageStateSuspended, MessageStateDischarging: + return true + default: + return false + } +} + +func isValidMessageStatus(fl validator.FieldLevel) bool { + priority := MessageStatus(fl.Field().String()) + switch priority { + case MessageStatusAccepted, MessageStatusUnknown: + return true + default: + return false + } +} + +// Contains message details, for a message to be displayed on a Charging Station. +type MessageInfo struct { + ID int `json:"id" validate:"gte=0"` // Master resource identifier, unique within an exchange context. It is defined within the OCPP context as a positive Integer value (greater or equal to zero). + Priority MessagePriority `json:"priority" validate:"required,messagePriority21"` // With what priority should this message be shown + State MessageState `json:"state,omitempty" validate:"omitempty,messageState21"` // During what state should this message be shown. When omitted this message should be shown in any state of the Charging Station. + StartDateTime *types.DateTime `json:"startDateTime,omitempty" validate:"omitempty"` // From what date-time should this message be shown. If omitted: directly. + EndDateTime *types.DateTime `json:"endDateTime,omitempty" validate:"omitempty"` // Until what date-time should this message be shown, after this date/time this message SHALL be removed. + TransactionID string `json:"transactionId,omitempty" validate:"omitempty,max=36"` // During which transaction shall this message be shown. Message SHALL be removed by the Charging Station after transaction has ended. + Message types.MessageContent `json:"message" validate:"required"` // Contains message details for the message to be displayed on a Charging Station. + Display *types.Component `json:"display,omitempty" validate:"omitempty"` // When a Charging Station has multiple Displays, this field can be used to define to which Display this message belongs. + MessageExtra []types.MessageContent `json:"messageExtra,omitempty" validate:"omitempty,max=4"` // Contains extra message details for the message to be displayed on a Charging Station. +} + +func init() { + _ = types.Validate.RegisterValidation("messagePriority21", isValidMessagePriority) + _ = types.Validate.RegisterValidation("messageState21", isValidMessageState) + _ = types.Validate.RegisterValidation("messageStatus21", isValidMessageStatus) +} diff --git a/ocpp2.1/firmware/firmware.go b/ocpp2.1/firmware/firmware.go new file mode 100644 index 00000000..c61a2bc9 --- /dev/null +++ b/ocpp2.1/firmware/firmware.go @@ -0,0 +1,33 @@ +// The firmware functional block contains OCPP 2.1 features that enable firmware updates on a charging station. +package firmware + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Firmware profile. +type CSMSHandler interface { + // OnFirmwareStatusNotification is called on the CSMS whenever a FirmwareStatusNotificationRequest is received from a charging station. + OnFirmwareStatusNotification(chargingStationID string, request *FirmwareStatusNotificationRequest) (response *FirmwareStatusNotificationResponse, err error) + // OnPublishFirmwareStatusNotification is called on the CSMS whenever a PublishFirmwareStatusNotificationRequest is received from a local controller. + OnPublishFirmwareStatusNotification(chargingStationID string, request *PublishFirmwareStatusNotificationRequest) (response *PublishFirmwareStatusNotificationResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Firmware profile. +type ChargingStationHandler interface { + // OnPublishFirmware is called on a charging station whenever a PublishFirmwareRequest is received from the CSMS. + OnPublishFirmware(request *PublishFirmwareRequest) (response *PublishFirmwareResponse, err error) + // OnUnpublishFirmware is called on a charging station whenever a UnpublishFirmwareRequest is received from the CSMS. + OnUnpublishFirmware(request *UnpublishFirmwareRequest) (response *UnpublishFirmwareResponse, err error) + // OnUpdateFirmware is called on a charging station whenever a UpdateFirmwareRequest is received from the CSMS. + OnUpdateFirmware(request *UpdateFirmwareRequest) (response *UpdateFirmwareResponse, err error) +} + +const ProfileName = "Firmware" + +var Profile = ocpp.NewProfile( + ProfileName, + FirmwareStatusNotificationFeature{}, + PublishFirmwareFeature{}, + PublishFirmwareStatusNotificationFeature{}, + UnpublishFirmwareFeature{}, + UpdateFirmwareFeature{}, +) diff --git a/ocpp2.1/firmware/firmware_status_notification.go b/ocpp2.1/firmware/firmware_status_notification.go new file mode 100644 index 00000000..74ab2565 --- /dev/null +++ b/ocpp2.1/firmware/firmware_status_notification.go @@ -0,0 +1,95 @@ +package firmware + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Firmware Status Notification (CS -> CSMS) -------------------- + +const FirmwareStatusNotificationFeatureName = "FirmwareStatusNotification" + +// Status reported in FirmwareStatusNotificationRequest. +type FirmwareStatus string + +const ( + 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, FirmwareStatusDownloadScheduled, FirmwareStatusDownloadPaused, + FirmwareStatusIdle, FirmwareStatusInstallationFailed, FirmwareStatusInstalling, FirmwareStatusInstalled, FirmwareStatusInstallRebooting, FirmwareStatusInstallScheduled, + FirmwareStatusInstallVerificationFailed, FirmwareStatusInvalidSignature, FirmwareStatusSignatureVerified: + return true + default: + return false + } +} + +// The field definition of the FirmwareStatusNotification request payload sent by the Charging Station to the CSMS. +type FirmwareStatusNotificationRequest struct { + Status FirmwareStatus `json:"status" validate:"required,firmwareStatus21"` + RequestID *int `json:"requestId,omitempty" validate:"omitempty,gte=0"` +} + +// This field definition of the FirmwareStatusNotification response payload, sent by the CSMS to the Charging Station in response to a FirmwareStatusNotificationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type FirmwareStatusNotificationResponse struct { +} + +// The Charging Station sends a notification to inform the CSMS about the progress of the downloading and installation of a firmware update. +// The Charging Station SHALL only send the status Idle after receipt of a TriggerMessage for a Firmware Status Notification, when it is not busy downloading/installing firmware. +// The FirmwareStatusNotification requests SHALL be sent to keep the CSMS updated with the status of the update process. +type FirmwareStatusNotificationFeature struct{} + +func (f FirmwareStatusNotificationFeature) GetFeatureName() string { + return FirmwareStatusNotificationFeatureName +} + +func (f FirmwareStatusNotificationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(FirmwareStatusNotificationRequest{}) +} + +func (f FirmwareStatusNotificationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(FirmwareStatusNotificationResponse{}) +} + +func (r FirmwareStatusNotificationRequest) GetFeatureName() string { + return FirmwareStatusNotificationFeatureName +} + +func (c FirmwareStatusNotificationResponse) GetFeatureName() string { + return FirmwareStatusNotificationFeatureName +} + +// Creates a new FirmwareStatusNotificationRequest, containing all required fields. Optional fields may be set afterwards. +func NewFirmwareStatusNotificationRequest(status FirmwareStatus) *FirmwareStatusNotificationRequest { + return &FirmwareStatusNotificationRequest{Status: status} +} + +// Creates a new FirmwareStatusNotificationResponse, which doesn't contain any required or optional fields. +func NewFirmwareStatusNotificationResponse() *FirmwareStatusNotificationResponse { + return &FirmwareStatusNotificationResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("firmwareStatus21", isValidFirmwareStatus) +} diff --git a/ocpp2.1/firmware/publish_firmware.go b/ocpp2.1/firmware/publish_firmware.go new file mode 100644 index 00000000..1e8b52af --- /dev/null +++ b/ocpp2.1/firmware/publish_firmware.go @@ -0,0 +1,68 @@ +package firmware + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Publish Firmware (CSMS -> CS) -------------------- + +const PublishFirmwareFeatureName = "PublishFirmware" + +// The field definition of the PublishFirmware request payload sent by the CSMS to the Charging Station. +type PublishFirmwareRequest struct { + Location string `json:"location" validate:"required,max=512"` // This contains a string containing a URI pointing to a location from which to retrieve the firmware. + Retries *int `json:"retries,omitempty" validate:"omitempty,gte=0"` // This specifies how many times Charging Station must try to download the firmware before giving up. If this field is not present, it is left to Charging Station to decide how many times it wants to retry. + Checksum string `json:"checksum" validate:"required,max=32"` // The MD5 checksum over the entire firmware file as a hexadecimal string of length 32. + RequestID int `json:"requestId" validate:"gte=0"` // The Id of the request. + RetryInterval *int `json:"retryInterval,omitempty" validate:"omitempty,gte=0"` // The interval in seconds after which a retry may be attempted. If this field is not present, it is left to Charging Station to decide how long to wait between attempts. +} + +// This field definition of the PublishFirmware response payload, sent by the Charging Station to the CSMS in response to a PublishFirmwareRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type PublishFirmwareResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS sends a PublishFirmwareRequest to instruct the Local Controller to download and publish the firmware, +// including an MD5 checksum of the firmware file. +// Upon receipt of PublishFirmwareRequest, the Local Controller responds with PublishFirmwareResponse. +// +// The local controller will download the firmware out-of-band and publish the URI of the updated firmware to +// the CSMS via a PublishFirmwareStatusNotificationRequest. +// +// Whenever the CSMS instructs charging stations to update their firmware, it will instruct to download the +// firmware form the local controller instead of from the CSMS, saving data and bandwidth on the WAN interface. +type PublishFirmwareFeature struct{} + +func (f PublishFirmwareFeature) GetFeatureName() string { + return PublishFirmwareFeatureName +} + +func (f PublishFirmwareFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(PublishFirmwareRequest{}) +} + +func (f PublishFirmwareFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(PublishFirmwareResponse{}) +} + +func (r PublishFirmwareRequest) GetFeatureName() string { + return PublishFirmwareFeatureName +} + +func (c PublishFirmwareResponse) GetFeatureName() string { + return PublishFirmwareFeatureName +} + +// Creates a new PublishFirmwareRequest, containing all required fields. Optional fields may be set afterwards. +func NewPublishFirmwareRequest(location string, checksum string, requestID int) *PublishFirmwareRequest { + return &PublishFirmwareRequest{Location: location, Checksum: checksum, RequestID: requestID} +} + +// Creates a new PublishFirmwareResponse, containing all required fields. Optional fields may be set afterwards. +func NewPublishFirmwareResponse(status types.GenericStatus) *PublishFirmwareResponse { + return &PublishFirmwareResponse{Status: status} +} diff --git a/ocpp2.1/firmware/publish_firmware_status_notification.go b/ocpp2.1/firmware/publish_firmware_status_notification.go new file mode 100644 index 00000000..b5ef4985 --- /dev/null +++ b/ocpp2.1/firmware/publish_firmware_status_notification.go @@ -0,0 +1,91 @@ +package firmware + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Publish Firmware Status Notification (CS -> CSMS) -------------------- + +const PublishFirmwareStatusNotificationFeatureName = "PublishFirmwareStatusNotification" + +// Status reported in PublishFirmwareStatusNotificationRequest. +type PublishFirmwareStatus string + +const ( + PublishFirmwareStatusIdle PublishFirmwareStatus = "Idle" + PublishFirmwareStatusDownloadScheduled PublishFirmwareStatus = "DownloadScheduled" + PublishFirmwareStatusDownloading PublishFirmwareStatus = "Downloading" + PublishFirmwareStatusDownloaded PublishFirmwareStatus = "Downloaded" + PublishFirmwareStatusPublished PublishFirmwareStatus = "Published" + PublishFirmwareStatusDownloadFailed PublishFirmwareStatus = "DownloadFailed" + PublishFirmwareStatusDownloadPaused PublishFirmwareStatus = "DownloadPaused" + PublishFirmwareStatusInvalidChecksum PublishFirmwareStatus = "InvalidChecksum" + PublishFirmwareStatusChecksumVerified PublishFirmwareStatus = "ChecksumVerified" + PublishFirmwareStatusPublishFailed PublishFirmwareStatus = "PublishFailed" +) + +func isValidPublishFirmwareStatus(fl validator.FieldLevel) bool { + status := PublishFirmwareStatus(fl.Field().String()) + switch status { + case PublishFirmwareStatusIdle, PublishFirmwareStatusDownloadScheduled, PublishFirmwareStatusDownloading, PublishFirmwareStatusDownloaded, PublishFirmwareStatusPublished, PublishFirmwareStatusDownloadFailed, PublishFirmwareStatusDownloadPaused, PublishFirmwareStatusInvalidChecksum, PublishFirmwareStatusChecksumVerified, PublishFirmwareStatusPublishFailed: + return true + default: + return false + } +} + +// The field definition of the PublishFirmwareStatusNotification request payload sent by the Charging Station to the CSMS. +type PublishFirmwareStatusNotificationRequest struct { + Status PublishFirmwareStatus `json:"status" validate:"required,publishFirmwareStatus21"` // This contains the progress status of the publishfirmware installation. + Location []string `json:"location,omitempty" validate:"omitempty,dive,max=512"` // Can be multiple URI’s, if the Local Controller supports e.g. HTTP, HTTPS, and FTP. + RequestID *int `json:"requestId,omitempty" validate:"omitempty,gte=0"` // The request id that was provided in the PublishFirmwareRequest which triggered this action. + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` // Additional information about the status, e.g. a description of the error. +} + +// This field definition of the PublishFirmwareStatusNotification response payload, sent by the CSMS to the Charging Station in response to a PublishFirmwareStatusNotificationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type PublishFirmwareStatusNotificationResponse struct { +} + +// The local controller sends a PublishFirmwareStatusNotificationRequest to inform the CSMS about the current PublishFirmware status. +// If the firmware was published correctly, the request will contain the location(s) URI(s) where the firmware was published at. +// +// The CSMS responds to each request with a PublishFirmwareStatusNotificationResponse. +type PublishFirmwareStatusNotificationFeature struct{} + +func (f PublishFirmwareStatusNotificationFeature) GetFeatureName() string { + return PublishFirmwareStatusNotificationFeatureName +} + +func (f PublishFirmwareStatusNotificationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(PublishFirmwareStatusNotificationRequest{}) +} + +func (f PublishFirmwareStatusNotificationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(PublishFirmwareStatusNotificationResponse{}) +} + +func (r PublishFirmwareStatusNotificationRequest) GetFeatureName() string { + return PublishFirmwareStatusNotificationFeatureName +} + +func (c PublishFirmwareStatusNotificationResponse) GetFeatureName() string { + return PublishFirmwareStatusNotificationFeatureName +} + +// Creates a new PublishFirmwareStatusNotificationRequest, containing all required fields. Optional fields may be set afterwards. +func NewPublishFirmwareStatusNotificationRequest(status PublishFirmwareStatus) *PublishFirmwareStatusNotificationRequest { + return &PublishFirmwareStatusNotificationRequest{Status: status} +} + +// Creates a new PublishFirmwareStatusNotificationResponse, which doesn't contain any required or optional fields. +func NewPublishFirmwareStatusNotificationResponse() *PublishFirmwareStatusNotificationResponse { + return &PublishFirmwareStatusNotificationResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("publishFirmwareStatus21", isValidPublishFirmwareStatus) +} diff --git a/ocpp2.1/firmware/unpublish_firmware.go b/ocpp2.1/firmware/unpublish_firmware.go new file mode 100644 index 00000000..b753100a --- /dev/null +++ b/ocpp2.1/firmware/unpublish_firmware.go @@ -0,0 +1,82 @@ +package firmware + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Publish Firmware (CSMS -> CS) -------------------- + +const UnpublishFirmwareFeatureName = "UnpublishFirmware" + +// Status for when stopping to publish a Firmware. +type UnpublishFirmwareStatus string + +const ( + UnpublishFirmwareStatusDownloadOngoing UnpublishFirmwareStatus = "DownloadOngoing" // Intermediate state. Firmware is being downloaded. + UnpublishFirmwareStatusNoFirmware UnpublishFirmwareStatus = "NoFirmware" // There is no published file. + UnpublishFirmwareStatusUnpublished UnpublishFirmwareStatus = "Unpublished" // Successful end state. Firmware file no longer being published. +) + +func isValidUnpublishFirmwareStatus(fl validator.FieldLevel) bool { + status := UnpublishFirmwareStatus(fl.Field().String()) + switch status { + case UnpublishFirmwareStatusDownloadOngoing, UnpublishFirmwareStatusNoFirmware, UnpublishFirmwareStatusUnpublished: + return true + default: + return false + } +} + +// The field definition of the UnpublishFirmware request payload sent by the CSMS to the Charging Station. +type UnpublishFirmwareRequest struct { + Checksum string `json:"checksum" validate:"required,max=32"` // The MD5 checksum over the entire firmware file as a hexadecimal string of length 32. +} + +// This field definition of the UnpublishFirmware response payload, sent by the Charging Station to the CSMS in response to a UnpublishFirmwareRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type UnpublishFirmwareResponse struct { + Status UnpublishFirmwareStatus `json:"status" validate:"required,unpublishFirmwareStatus21"` +} + +// Allows to stop a Local Controller from publishing a firmware update to connected Charging Stations. +// The CSMS sends an UnpublishFirmwareRequest to instruct the local controller to unpublish the firmware. +// The local controller unpublishes the firmware, then responds with an UnpublishFirmwareResponse. +type UnpublishFirmwareFeature struct{} + +func (f UnpublishFirmwareFeature) GetFeatureName() string { + return UnpublishFirmwareFeatureName +} + +func (f UnpublishFirmwareFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(UnpublishFirmwareRequest{}) +} + +func (f UnpublishFirmwareFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(UnpublishFirmwareResponse{}) +} + +func (r UnpublishFirmwareRequest) GetFeatureName() string { + return UnpublishFirmwareFeatureName +} + +func (c UnpublishFirmwareResponse) GetFeatureName() string { + return UnpublishFirmwareFeatureName +} + +// Creates a new UnpublishFirmwareRequest, containing all required fields. There are no optional fields for this message. +func NewUnpublishFirmwareRequest(checksum string) *UnpublishFirmwareRequest { + return &UnpublishFirmwareRequest{Checksum: checksum} +} + +// Creates a new UnpublishFirmwareResponse, containing all required fields. There are no optional fields for this message. +func NewUnpublishFirmwareResponse(status UnpublishFirmwareStatus) *UnpublishFirmwareResponse { + return &UnpublishFirmwareResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("unpublishFirmwareStatus21", isValidUnpublishFirmwareStatus) +} diff --git a/ocpp2.1/firmware/update_firmware.go b/ocpp2.1/firmware/update_firmware.go new file mode 100644 index 00000000..508c163f --- /dev/null +++ b/ocpp2.1/firmware/update_firmware.go @@ -0,0 +1,106 @@ +package firmware + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Publish Firmware (CSMS -> CS) -------------------- + +const UpdateFirmwareFeatureName = "UpdateFirmware" + +// Indicates whether the Charging Station was able to accept the request. +type UpdateFirmwareStatus string + +const ( + UpdateFirmwareStatusAccepted UpdateFirmwareStatus = "Accepted" + UpdateFirmwareStatusRejected UpdateFirmwareStatus = "Rejected" + UpdateFirmwareStatusAcceptedCanceled UpdateFirmwareStatus = "AcceptedCanceled" + UpdateFirmwareStatusInvalidCertificate UpdateFirmwareStatus = "InvalidCertificate" + UpdateFirmwareStatusRevokedCertificate UpdateFirmwareStatus = "RevokedCertificate" +) + +func isValidUpdateFirmwareStatus(fl validator.FieldLevel) bool { + status := UpdateFirmwareStatus(fl.Field().String()) + switch status { + case UpdateFirmwareStatusAccepted, + UpdateFirmwareStatusRejected, + UpdateFirmwareStatusAcceptedCanceled, + UpdateFirmwareStatusInvalidCertificate, + UpdateFirmwareStatusRevokedCertificate: + return true + default: + return false + } +} + +// Represents a copy of the firmware that can be loaded/updated on the Charging Station. +type Firmware struct { + Location string `json:"location" validate:"required,max=512,uri"` // URI defining the origin of the firmware. + RetrieveDateTime *types.DateTime `json:"retrieveDateTime" validate:"required"` // Date and time at which the firmware shall be retrieved. + InstallDateTime *types.DateTime `json:"installDateTime,omitempty" validate:"omitempty"` // Date and time at which the firmware shall be installed. + SigningCertificate string `json:"signingCertificate,omitempty" validate:"max=5500"` // Certificate with which the firmware was signed. PEM encoded X.509 certificate. + Signature string `json:"signature,omitempty" validate:"max=800"` // Base64 encoded firmware signature. +} + +// The field definition of the UpdateFirmware request payload sent by the CSMS to the Charging Station. +type UpdateFirmwareRequest struct { + Retries *int `json:"retries,omitempty" validate:"omitempty,gte=0"` // This specifies how many times Charging Station must try to download the firmware before giving up. If this field is not present, it is left to Charging Station to decide how many times it wants to retry. + RetryInterval *int `json:"retryInterval,omitempty" validate:"omitempty,gte=0"` // The interval in seconds after which a retry may be attempted. If this field is not present, it is left to Charging Station to decide how long to wait between attempts. + RequestID int `json:"requestId" validate:"gte=0"` // The Id of the request. + Firmware Firmware `json:"firmware" validate:"required"` // Specifies the firmware to be updated on the Charging Station. +} + +// This field definition of the UpdateFirmware response payload, sent by the Charging Station to the CSMS in response to a UpdateFirmwareRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type UpdateFirmwareResponse struct { + Status UpdateFirmwareStatus `json:"status" validate:"required,updateFirmwareStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// A CSMS may instruct a Charging Station to update its firmware, by downloading and installing a new version. +// The CSMS sends an UpdateFirmwareRequest message that contains the location of the firmware, +// the time after which it should be retrieved, and information on how many times the +// Charging Station should retry downloading the firmware. +// +// The Charging station responds with an UpdateFirmwareResponse and then starts downloading the firmware. +// During the download/install procedure, the charging station shall notify the CSMS of its current status +// by sending FirmwareStatusNotification messages. +type UpdateFirmwareFeature struct{} + +func (f UpdateFirmwareFeature) GetFeatureName() string { + return UpdateFirmwareFeatureName +} + +func (f UpdateFirmwareFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(UpdateFirmwareRequest{}) +} + +func (f UpdateFirmwareFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(UpdateFirmwareResponse{}) +} + +func (r UpdateFirmwareRequest) GetFeatureName() string { + return UpdateFirmwareFeatureName +} + +func (c UpdateFirmwareResponse) GetFeatureName() string { + return UpdateFirmwareFeatureName +} + +// Creates a new UpdateFirmwareRequest, containing all required fields. Optional fields may be set afterwards. +func NewUpdateFirmwareRequest(requestID int, firmware Firmware) *UpdateFirmwareRequest { + return &UpdateFirmwareRequest{RequestID: requestID, Firmware: firmware} +} + +// Creates a new UpdateFirmwareResponse, containing all required fields. Optional fields may be set afterwards. +func NewUpdateFirmwareResponse(status UpdateFirmwareStatus) *UpdateFirmwareResponse { + return &UpdateFirmwareResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("updateFirmwareStatus21", isValidUpdateFirmwareStatus) +} diff --git a/ocpp2.1/iso15118/delete_certificate.go b/ocpp2.1/iso15118/delete_certificate.go new file mode 100644 index 00000000..4d1e8793 --- /dev/null +++ b/ocpp2.1/iso15118/delete_certificate.go @@ -0,0 +1,82 @@ +package iso15118 + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Delete Certificate (CSMS -> CS) -------------------- + +const DeleteCertificateFeatureName = "DeleteCertificate" + +// Status returned in response to DeleteCertificateRequest. +type DeleteCertificateStatus string + +const ( + DeleteCertificateStatusAccepted DeleteCertificateStatus = "Accepted" + DeleteCertificateStatusFailed DeleteCertificateStatus = "Failed" + DeleteCertificateStatusNotFound DeleteCertificateStatus = "NotFound" +) + +func isValidDeleteCertificateStatus(fl validator.FieldLevel) bool { + status := DeleteCertificateStatus(fl.Field().String()) + switch status { + case DeleteCertificateStatusAccepted, DeleteCertificateStatusFailed, DeleteCertificateStatusNotFound: + return true + default: + return false + } +} + +// The field definition of the DeleteCertificate request payload sent by the CSMS to the Charging Station. +type DeleteCertificateRequest struct { + CertificateHashData types.CertificateHashData `json:"certificateHashData" validate:"required"` +} + +// This field definition of the DeleteCertificate response payload, sent by the Charging Station to the CSMS in response to a DeleteCertificateRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type DeleteCertificateResponse struct { + Status DeleteCertificateStatus `json:"status" validate:"required,deleteCertificateStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS requests the Charging Station to delete a specific installed certificate by sending a DeleteCertificateRequest. +// The Charging Station responds with a DeleteCertificateResponse. +type DeleteCertificateFeature struct{} + +func (f DeleteCertificateFeature) GetFeatureName() string { + return DeleteCertificateFeatureName +} + +func (f DeleteCertificateFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(DeleteCertificateRequest{}) +} + +func (f DeleteCertificateFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(DeleteCertificateResponse{}) +} + +func (r DeleteCertificateRequest) GetFeatureName() string { + return DeleteCertificateFeatureName +} + +func (c DeleteCertificateResponse) GetFeatureName() string { + return DeleteCertificateFeatureName +} + +// Creates a new DeleteCertificateRequest, containing all required fields. There are no optional fields for this message. +func NewDeleteCertificateRequest(certificateHashData types.CertificateHashData) *DeleteCertificateRequest { + return &DeleteCertificateRequest{CertificateHashData: certificateHashData} +} + +// Creates a new DeleteCertificateResponse, containing all required fields. Optional fields may be set afterwards. +func NewDeleteCertificateResponse(status DeleteCertificateStatus) *DeleteCertificateResponse { + return &DeleteCertificateResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("deleteCertificateStatus21", isValidDeleteCertificateStatus) +} diff --git a/ocpp2.1/iso15118/get_15118ev_certificate.go b/ocpp2.1/iso15118/get_15118ev_certificate.go new file mode 100644 index 00000000..1866ac15 --- /dev/null +++ b/ocpp2.1/iso15118/get_15118ev_certificate.go @@ -0,0 +1,88 @@ +package iso15118 + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Get 15118EV Certificate (CS -> CSMS) -------------------- + +const Get15118EVCertificateFeatureName = "Get15118EVCertificate" + +// Defines whether certificate needs to be installed or updated. +type CertificateAction string + +const ( + CertificateActionInstall CertificateAction = "Install" + CertificateActionUpdate CertificateAction = "Update" +) + +func isValidCertificateAction(fl validator.FieldLevel) bool { + status := CertificateAction(fl.Field().String()) + switch status { + case CertificateActionInstall, CertificateActionUpdate: + return true + default: + return false + } +} + +// The field definition of the Get15118EVCertificate request payload sent by the Charging Station to the CSMS. +type Get15118EVCertificateRequest struct { + SchemaVersion string `json:"iso15118SchemaVersion" validate:"required,max=50"` + Action CertificateAction `json:"action" validate:"required,certificateAction21"` + ExiRequest string `json:"exiRequest" validate:"required,max=11000"` + MaximumContractCertificateChains *int `json:"maximumContractCertificateChains,omitempty" validate:"omitempty,min=0"` + PrioritizedEMAIDs []string `json:"prioritizedEMAIDs,omitempty" validate:"omitempty,max=8"` +} + +// This field definition of the Get15118EVCertificate response payload, sent by the CSMS to the Charging Station in response to a Get15118EVCertificateRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type Get15118EVCertificateResponse struct { + Status types.Certificate15118EVStatus `json:"status" validate:"required,15118EVCertificate21"` + ExiResponse string `json:"exiResponse" validate:"required,max=17000"` // Raw CertificateInstallationRes response for the EV, Base64 encoded. + RemainingContracts *int `json:"remainingContracts,omitempty" validate:"omitempty,gte=0"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// An EV connected to a Charging Station may request a new certificate. +// The EV initiates installing a new certificate. The Charging Station forwards the request for a new certificate to the CSMS. +// The CSMS responds to Charging Station with a message containing the status and optionally new certificate. +type Get15118EVCertificateFeature struct{} + +func (f Get15118EVCertificateFeature) GetFeatureName() string { + return Get15118EVCertificateFeatureName +} + +func (f Get15118EVCertificateFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(Get15118EVCertificateRequest{}) +} + +func (f Get15118EVCertificateFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(Get15118EVCertificateResponse{}) +} + +func (r Get15118EVCertificateRequest) GetFeatureName() string { + return Get15118EVCertificateFeatureName +} + +func (c Get15118EVCertificateResponse) GetFeatureName() string { + return Get15118EVCertificateFeatureName +} + +// Creates a new Get15118EVCertificateRequest, containing all required fields. There are no optional fields for this message. +func NewGet15118EVCertificateRequest(schemaVersion string, action CertificateAction, exiRequest string) *Get15118EVCertificateRequest { + return &Get15118EVCertificateRequest{SchemaVersion: schemaVersion, Action: action, ExiRequest: exiRequest} +} + +// Creates a new Get15118EVCertificateResponse, containing all required fields. +func NewGet15118EVCertificateResponse(status types.Certificate15118EVStatus, exiResponse string) *Get15118EVCertificateResponse { + return &Get15118EVCertificateResponse{Status: status, ExiResponse: exiResponse} +} + +func init() { + _ = types.Validate.RegisterValidation("certificateAction21", isValidCertificateAction) +} diff --git a/ocpp2.1/iso15118/get_certificate_status.go b/ocpp2.1/iso15118/get_certificate_status.go new file mode 100644 index 00000000..60aa680f --- /dev/null +++ b/ocpp2.1/iso15118/get_certificate_status.go @@ -0,0 +1,61 @@ +package iso15118 + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Get Certificate Status (CS -> CSMS) -------------------- + +const GetCertificateStatusFeatureName = "GetCertificateStatus" + +// The field definition of the GetCertificateStatus request payload sent by the Charging Station to the CSMS. +type GetCertificateStatusRequest struct { + OcspRequestData types.OCSPRequestDataType `json:"ocspRequestData" validate:"required"` +} + +// This field definition of the GetCertificateStatus response payload, sent by the CSMS to the Charging Station in response to a GetCertificateStatusRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetCertificateStatusResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + OcspResult string `json:"ocspResult,omitempty" validate:"omitempty,max=18000"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// For 15118 certificate installation on EVs, the Charging Station requests the CSMS to provide the OCSP certificate +// status for its 15118 certificates. +// The CSMS responds with a GetCertificateStatusResponse, containing the OCSP certificate status. +// The status indicator in the GetCertificateStatusResponse indicates whether or not the CSMS was successful in retrieving the certificate status. +// It does NOT indicate the validity of the certificate. +type GetCertificateStatusFeature struct{} + +func (f GetCertificateStatusFeature) GetFeatureName() string { + return GetCertificateStatusFeatureName +} + +func (f GetCertificateStatusFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetCertificateStatusRequest{}) +} + +func (f GetCertificateStatusFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetCertificateStatusResponse{}) +} + +func (r GetCertificateStatusRequest) GetFeatureName() string { + return GetCertificateStatusFeatureName +} + +func (c GetCertificateStatusResponse) GetFeatureName() string { + return GetCertificateStatusFeatureName +} + +// Creates a new GetCertificateStatusRequest, containing all required fields. There are no optional fields for this message. +func NewGetCertificateStatusRequest(ocspRequestData types.OCSPRequestDataType) *GetCertificateStatusRequest { + return &GetCertificateStatusRequest{OcspRequestData: ocspRequestData} +} + +// Creates a new GetCertificateStatusResponse, containing all required fields. Optional fields may be set afterwards. +func NewGetCertificateStatusResponse(status types.GenericStatus) *GetCertificateStatusResponse { + return &GetCertificateStatusResponse{Status: status} +} diff --git a/ocpp2.1/iso15118/get_installed_certificate_ids.go b/ocpp2.1/iso15118/get_installed_certificate_ids.go new file mode 100644 index 00000000..7a9d2970 --- /dev/null +++ b/ocpp2.1/iso15118/get_installed_certificate_ids.go @@ -0,0 +1,81 @@ +package iso15118 + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Installed Certificate IDs (CSMS -> CS) -------------------- + +const GetInstalledCertificateIdsFeatureName = "GetInstalledCertificateIds" + +// Status returned in response to GetInstalledCertificateIdsRequest, that indicates whether certificate signing has been accepted or rejected. +type GetInstalledCertificateStatus string + +const ( + GetInstalledCertificateStatusAccepted GetInstalledCertificateStatus = "Accepted" // Normal successful completion (no errors). + GetInstalledCertificateStatusNotFound GetInstalledCertificateStatus = "NotFound" // Requested resource not found +) + +func isValidGetInstalledCertificateStatus(fl validator.FieldLevel) bool { + status := GetInstalledCertificateStatus(fl.Field().String()) + switch status { + case GetInstalledCertificateStatusAccepted, GetInstalledCertificateStatusNotFound: + return true + default: + return false + } +} + +// The field definition of the GetInstalledCertificateIdsRequest PDU sent by the CSMS to the Charging Station. +type GetInstalledCertificateIdsRequest struct { + CertificateTypes []types.CertificateUse `json:"certificateType" validate:"omitempty,dive,certificateUse21"` +} + +// 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,getInstalledCertificateStatus21"` + 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. +// The CSMS requests the Charging Station to send a list of installed certificates by sending a GetInstalledCertificateIdsRequest. +// The Charging Station responds with a GetInstalledCertificateIdsResponse. +type GetInstalledCertificateIdsFeature struct{} + +func (f GetInstalledCertificateIdsFeature) GetFeatureName() string { + return GetInstalledCertificateIdsFeatureName +} + +func (f GetInstalledCertificateIdsFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetInstalledCertificateIdsRequest{}) +} + +func (f GetInstalledCertificateIdsFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetInstalledCertificateIdsResponse{}) +} + +func (r GetInstalledCertificateIdsRequest) GetFeatureName() string { + return GetInstalledCertificateIdsFeatureName +} + +func (c GetInstalledCertificateIdsResponse) GetFeatureName() string { + return GetInstalledCertificateIdsFeatureName +} + +// Creates a new GetInstalledCertificateIdsRequest, containing all required fields. There are no optional fields for this message. +func NewGetInstalledCertificateIdsRequest() *GetInstalledCertificateIdsRequest { + return &GetInstalledCertificateIdsRequest{} +} + +// Creates a new NewGetInstalledCertificateIdsResponse, containing all required fields. Additional optional fields may be set afterwards. +func NewGetInstalledCertificateIdsResponse(status GetInstalledCertificateStatus) *GetInstalledCertificateIdsResponse { + return &GetInstalledCertificateIdsResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("getInstalledCertificateStatus21", isValidGetInstalledCertificateStatus) +} diff --git a/ocpp2.1/iso15118/install_certificate.go b/ocpp2.1/iso15118/install_certificate.go new file mode 100644 index 00000000..8624df2a --- /dev/null +++ b/ocpp2.1/iso15118/install_certificate.go @@ -0,0 +1,85 @@ +package iso15118 + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Clear Display (CSMS -> CS) -------------------- + +const InstallCertificateFeatureName = "InstallCertificate" + +// Charging Station indicates if installation was successful. +type InstallCertificateStatus string + +const ( + CertificateStatusAccepted InstallCertificateStatus = "Accepted" + CertificateStatusRejected InstallCertificateStatus = "Rejected" + CertificateStatusFailed InstallCertificateStatus = "Failed" +) + +func isValidInstallCertificateStatus(fl validator.FieldLevel) bool { + status := InstallCertificateStatus(fl.Field().String()) + switch status { + case CertificateStatusAccepted, CertificateStatusRejected, CertificateStatusFailed: + return true + default: + return false + } +} + +// The field definition of the InstallCertificate request payload sent by the CSMS to the Charging Station. +type InstallCertificateRequest struct { + CertificateType types.CertificateUse `json:"certificateType" validate:"required,certificateUse21"` // Indicates the certificate type that is sent. + Certificate string `json:"certificate" validate:"required,max=5500"` // A PEM encoded X.509 certificate. +} + +// This field definition of the InstallCertificate response payload, sent by the Charging Station to the CSMS in response to a InstallCertificateRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type InstallCertificateResponse struct { + Status InstallCertificateStatus `json:"status" validate:"required,installCertificateStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS requests the Charging Station to install a new certificate by sending an InstallCertificateRequest. +// The certificate may be a root CA certificate, a Sub-CA certificate for an eMobility Operator, Charging Station operator, or a V2G root certificate. +// +// The Charging Station responds with an InstallCertificateResponse. +type InstallCertificateFeature struct{} + +func (f InstallCertificateFeature) GetFeatureName() string { + return InstallCertificateFeatureName +} + +func (f InstallCertificateFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(InstallCertificateRequest{}) +} + +func (f InstallCertificateFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(InstallCertificateResponse{}) +} + +func (r InstallCertificateRequest) GetFeatureName() string { + return InstallCertificateFeatureName +} + +func (c InstallCertificateResponse) GetFeatureName() string { + return InstallCertificateFeatureName +} + +// Creates a new InstallCertificateRequest, containing all required fields. There are no optional fields for this message. +func NewInstallCertificateRequest(certificateType types.CertificateUse, certificate string) *InstallCertificateRequest { + return &InstallCertificateRequest{CertificateType: certificateType, Certificate: certificate} +} + +// Creates a new InstallCertificateResponse, containing all required fields. There are no optional fields for this message. +func NewInstallCertificateResponse(status InstallCertificateStatus) *InstallCertificateResponse { + return &InstallCertificateResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("installCertificateStatus21", isValidInstallCertificateStatus) +} diff --git a/ocpp2.1/iso15118/iso_15118.go b/ocpp2.1/iso15118/iso_15118.go new file mode 100644 index 00000000..514413d2 --- /dev/null +++ b/ocpp2.1/iso15118/iso_15118.go @@ -0,0 +1,37 @@ +// The ISO 15118 functional block contains OCPP 2.1 features that allow: +// +// - communication between EV and an EVSE +// +// - support for certificate-based authentication and authorization at the charging station, i.e. plug and charge +package iso15118 + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 ISO 15118 profile. +type CSMSHandler interface { + // OnGet15118EVCertificate is called on the CSMS whenever a Get15118EVCertificateRequest is received from a charging station. + OnGet15118EVCertificate(chargingStationID string, request *Get15118EVCertificateRequest) (response *Get15118EVCertificateResponse, err error) + // OnGetCertificateStatus is called on the CSMS whenever a GetCertificateStatusRequest is received from a charging station. + OnGetCertificateStatus(chargingStationID string, request *GetCertificateStatusRequest) (response *GetCertificateStatusResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 ISO 15118 profile. +type ChargingStationHandler interface { + // OnDeleteCertificate is called on a charging station whenever a DeleteCertificateRequest is received from the CSMS. + OnDeleteCertificate(request *DeleteCertificateRequest) (response *DeleteCertificateResponse, err error) + // OnGetInstalledCertificateIds is called on a charging station whenever a GetInstalledCertificateIdsRequest is received from the CSMS. + OnGetInstalledCertificateIds(request *GetInstalledCertificateIdsRequest) (response *GetInstalledCertificateIdsResponse, err error) + // OnInstallCertificate is called on a charging station whenever an InstallCertificateRequest is received from the CSMS. + OnInstallCertificate(request *InstallCertificateRequest) (response *InstallCertificateResponse, err error) +} + +const ProfileName = "ISO15118" + +var Profile = ocpp.NewProfile( + ProfileName, + DeleteCertificateFeature{}, + Get15118EVCertificateFeature{}, + GetCertificateStatusFeature{}, + GetInstalledCertificateIdsFeature{}, + InstallCertificateFeature{}, +) diff --git a/ocpp2.1/localauth/get_local_list_version.go b/ocpp2.1/localauth/get_local_list_version.go new file mode 100644 index 00000000..bfcb57b5 --- /dev/null +++ b/ocpp2.1/localauth/get_local_list_version.go @@ -0,0 +1,54 @@ +package localauth + +import ( + "reflect" +) + +// -------------------- Get Local List Version (CSMS -> CS) -------------------- + +const GetLocalListVersionFeatureName = "GetLocalListVersion" + +// The field definition of the GetLocalListVersion request payload sent by the CSMS to the Charging Station. +type GetLocalListVersionRequest struct { +} + +// This field definition of the GetLocalListVersion response payload, sent by the Charging Station to the CSMS in response to a GetLocalListVersionRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetLocalListVersionResponse struct { + VersionNumber int `json:"versionNumber" validate:"gte=0"` +} + +// The CSMS can request a Charging Station for the version number of the Local Authorization List by sending a GetLocalListVersionRequest. +// Upon receipt of the GetLocalListVersionRequest Charging Station responds with a GetLocalListVersionResponse containing the version number of its Local Authorization List. +// The Charging Station SHALL use a version number of 0 (zero) to indicate that the Local Authorization List is empty. +type GetLocalListVersionFeature struct{} + +func (f GetLocalListVersionFeature) GetFeatureName() string { + return GetLocalListVersionFeatureName +} + +func (f GetLocalListVersionFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetLocalListVersionRequest{}) +} + +func (f GetLocalListVersionFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetLocalListVersionResponse{}) +} + +func (r GetLocalListVersionRequest) GetFeatureName() string { + return GetLocalListVersionFeatureName +} + +func (c GetLocalListVersionResponse) GetFeatureName() string { + return GetLocalListVersionFeatureName +} + +// Creates a new GetLocalListVersionRequest, which doesn't contain any required or optional fields. +func NewGetLocalListVersionRequest() *GetLocalListVersionRequest { + return &GetLocalListVersionRequest{} +} + +// Creates a new GetLocalListVersionResponse, containing all required fields. There are no optional fields for this message. +func NewGetLocalListVersionResponse(version int) *GetLocalListVersionResponse { + return &GetLocalListVersionResponse{VersionNumber: version} +} diff --git a/ocpp2.1/localauth/local_auth_list.go b/ocpp2.1/localauth/local_auth_list.go new file mode 100644 index 00000000..96b0b4f3 --- /dev/null +++ b/ocpp2.1/localauth/local_auth_list.go @@ -0,0 +1,25 @@ +// The Local authorization list functional block contains OCPP 2.1 features for synchronizing local authorization lists between CSMS and charging station. +// Local lists are used for offline and generally optimized authorization. +package localauth + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Local Authorization List profile. +type CSMSHandler interface { +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Local Authorization List profile. +type ChargingStationHandler interface { + // OnGetLocalListVersion is called on a charging station whenever a GetLocalListVersionRequest is received from the CSMS. + OnGetLocalListVersion(request *GetLocalListVersionRequest) (response *GetLocalListVersionResponse, err error) + // OnSendLocalList is called on a charging station whenever a SendLocalListRequest is received from the CSMS. + OnSendLocalList(request *SendLocalListRequest) (response *SendLocalListResponse, err error) +} + +const ProfileName = "LocalAuthList" + +var Profile = ocpp.NewProfile( + ProfileName, + GetLocalListVersionFeature{}, + SendLocalListFeature{}, +) diff --git a/ocpp2.1/localauth/send_local_list.go b/ocpp2.1/localauth/send_local_list.go new file mode 100644 index 00000000..ac260a8b --- /dev/null +++ b/ocpp2.1/localauth/send_local_list.go @@ -0,0 +1,115 @@ +package localauth + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Send Local List (CSMS -> CS) -------------------- + +const SendLocalListFeatureName = "SendLocalList" + +// Indicates the type of update (full or differential) for a SendLocalListRequest. +type UpdateType string + +const ( + UpdateTypeDifferential UpdateType = "Differential" // Indicates that the current Local Authorization List must be updated with the values in this message. + UpdateTypeFull UpdateType = "Full" // Indicates that the current Local Authorization List must be replaced by the values in this message. +) + +func isValidUpdateType(fl validator.FieldLevel) bool { + status := UpdateType(fl.Field().String()) + switch status { + case UpdateTypeDifferential, UpdateTypeFull: + return true + default: + return false + } +} + +// Indicates whether the Charging Station has successfully received and applied the update of the Local Authorization List. +type SendLocalListStatus string + +const ( + SendLocalListStatusAccepted SendLocalListStatus = "Accepted" // Local Authorization List successfully updated. + SendLocalListStatusFailed SendLocalListStatus = "Failed" // Failed to update the Local Authorization List. + SendLocalListStatusVersionMismatch SendLocalListStatus = "VersionMismatch" // Version number in the request for a differential update is less or equal then version number of current list. +) + +func isValidSendLocalListStatus(fl validator.FieldLevel) bool { + status := SendLocalListStatus(fl.Field().String()) + switch status { + case SendLocalListStatusAccepted, SendLocalListStatusFailed, SendLocalListStatusVersionMismatch: + return true + default: + return false + } +} + +// Contains the identifier to use for authorization. +type AuthorizationData struct { + IdTokenInfo *types.IdTokenInfo `json:"idTokenInfo,omitempty" validate:"omitempty"` + IdToken types.IdToken `json:"idToken"` +} + +// The field definition of the SendLocalList request payload sent by the CSMS to the Charging Station. +type SendLocalListRequest struct { + VersionNumber int `json:"versionNumber" validate:"gte=0"` + UpdateType UpdateType `json:"updateType" validate:"required,updateType21"` + LocalAuthorizationList []AuthorizationData `json:"localAuthorizationList,omitempty" validate:"omitempty,dive"` +} + +// This field definition of the SendLocalList response payload, sent by the Charging Station to the CSMS in response to a SendLocalListRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SendLocalListResponse struct { + Status SendLocalListStatus `json:"status" validate:"required,sendLocalListStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// Enables the CSMS to send a Local Authorization List which a Charging Station can use for the +// authorization of idTokens. +// The list MAY be either a full list to replace the current list in the Charging Station or it MAY +// be a differential list with updates to be applied to the current list in the Charging Station. +// +// To install or update a local authorization list, the CSMS sends a SendLocalListRequest to a +// Charging Station, which responds with a SendLocalListResponse, containing the status of the operation. +// +// If LocalAuthListEnabled is configured to false on a charging station, this operation will have no effect. +type SendLocalListFeature struct{} + +func (f SendLocalListFeature) GetFeatureName() string { + return SendLocalListFeatureName +} + +func (f SendLocalListFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SendLocalListRequest{}) +} + +func (f SendLocalListFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SendLocalListResponse{}) +} + +func (r SendLocalListRequest) GetFeatureName() string { + return SendLocalListFeatureName +} + +func (c SendLocalListResponse) GetFeatureName() string { + return SendLocalListFeatureName +} + +// Creates a new SendLocalListRequest, which doesn't contain any required or optional fields. +func NewSendLocalListRequest(versionNumber int, updateType UpdateType) *SendLocalListRequest { + return &SendLocalListRequest{VersionNumber: versionNumber, UpdateType: updateType} +} + +// Creates a new SendLocalListResponse, containing all required fields. There are no optional fields for this message. +func NewSendLocalListResponse(status SendLocalListStatus) *SendLocalListResponse { + return &SendLocalListResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("updateType21", isValidUpdateType) + _ = types.Validate.RegisterValidation("sendLocalListStatus21", isValidSendLocalListStatus) +} diff --git a/ocpp2.1/meter/meter.go b/ocpp2.1/meter/meter.go new file mode 100644 index 00000000..94e16581 --- /dev/null +++ b/ocpp2.1/meter/meter.go @@ -0,0 +1,21 @@ +// The Meter values functional block contains OCPP 2.1 features for sending meter values to the CSMS. +package meter + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Meter values profile. +type CSMSHandler interface { + // OnMeterValues is called on the CSMS whenever a MeterValuesRequest is received from a charging station. + OnMeterValues(chargingStationID string, request *MeterValuesRequest) (response *MeterValuesResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Meter values profile. +type ChargingStationHandler interface { +} + +const ProfileName = "Meter" + +var Profile = ocpp.NewProfile( + ProfileName, + MeterValuesFeature{}, +) diff --git a/ocpp2.1/meter/meter_values.go b/ocpp2.1/meter/meter_values.go new file mode 100644 index 00000000..2b264176 --- /dev/null +++ b/ocpp2.1/meter/meter_values.go @@ -0,0 +1,63 @@ +package meter + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Meter Values (CS -> CSMS) -------------------- + +const MeterValuesFeatureName = "MeterValues" + +// The field definition of the MeterValues request payload sent by the Charge Point to the Central System. +type MeterValuesRequest struct { + EvseID int `json:"evseId" validate:"gte=0"` // This contains a number (>0) designating an EVSE of the Charging Station. ‘0’ (zero) is used to designate the main power meter. + MeterValue []types.MeterValue `json:"meterValue" validate:"required,min=1,dive"` +} + +// This field definition of the Authorize confirmation payload, sent by the Charge Point to the Central System in response to an AuthorizeRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type MeterValuesResponse struct { +} + +// The message is used to sample the electrical meter or other sensor/transducer hardware to provide information about the Charging Stations' Meter Values, outside of a transaction. +// The Charging Station is configured to send Meter values every XX seconds. +// +// The Charging Station samples the electrical meter or other sensor/transducer hardware to provide information about its Meter Values. +// Depending on configuration settings, the Charging Station MAY send a MeterValues request, for offloading Meter Values to the CSMS. +// Upon receipt of a MeterValuesRequest message, the CSMS responds with a MeterValuesResponse message +// +// The MeterValuesRequest and MeterValuesResponse messages are deprecated in OCPP 2.0. +// It is advised to start using Device Management Monitoring instead, see the diagnostics functional block. +type MeterValuesFeature struct{} + +func (f MeterValuesFeature) GetFeatureName() string { + return MeterValuesFeatureName +} + +func (f MeterValuesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(MeterValuesRequest{}) +} + +func (f MeterValuesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(MeterValuesResponse{}) +} + +func (r MeterValuesRequest) GetFeatureName() string { + return MeterValuesFeatureName +} + +func (c MeterValuesResponse) GetFeatureName() string { + return MeterValuesFeatureName +} + +// Creates a new MeterValuesRequest, containing all required fields. Optional fields may be set afterwards. +func NewMeterValuesRequest(evseID int, meterValues []types.MeterValue) *MeterValuesRequest { + return &MeterValuesRequest{EvseID: evseID, MeterValue: meterValues} +} + +// Creates a new MeterValuesResponse, which doesn't contain any required or optional fields. +func NewMeterValuesResponse() *MeterValuesResponse { + return &MeterValuesResponse{} +} diff --git a/ocpp2.1/provisioning/boot_notification.go b/ocpp2.1/provisioning/boot_notification.go new file mode 100644 index 00000000..2c031b77 --- /dev/null +++ b/ocpp2.1/provisioning/boot_notification.go @@ -0,0 +1,137 @@ +package provisioning + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Boot Notification (CS -> CSMS) -------------------- + +const BootNotificationFeatureName = "BootNotification" + +// Result of registration in response to a BootNotification request. +type RegistrationStatus string + +const ( + RegistrationStatusAccepted RegistrationStatus = "Accepted" + RegistrationStatusPending RegistrationStatus = "Pending" + RegistrationStatusRejected RegistrationStatus = "Rejected" +) + +// The reason for sending a BootNotification event to the CSMS. +type BootReason string + +const ( + BootReasonApplicationReset BootReason = "ApplicationReset" + BootReasonFirmwareUpdate BootReason = "FirmwareUpdate" + BootReasonLocalReset BootReason = "LocalReset" + BootReasonPowerUp BootReason = "PowerUp" + BootReasonRemoteReset BootReason = "RemoteReset" + BootReasonScheduledReset BootReason = "ScheduledReset" + BootReasonTriggered BootReason = "Triggered" + BootReasonUnknown BootReason = "Unknown" + BootReasonWatchdog BootReason = "Watchdog" +) + +func isValidRegistrationStatus(fl validator.FieldLevel) bool { + status := RegistrationStatus(fl.Field().String()) + switch status { + case RegistrationStatusAccepted, RegistrationStatusPending, RegistrationStatusRejected: + return true + default: + return false + } +} + +func isValidBootReason(fl validator.FieldLevel) bool { + reason := BootReason(fl.Field().String()) + switch reason { + case BootReasonApplicationReset, BootReasonFirmwareUpdate, BootReasonLocalReset, + BootReasonPowerUp, BootReasonRemoteReset, BootReasonScheduledReset, BootReasonTriggered, + BootReasonUnknown, BootReasonWatchdog: + return true + default: + return false + } +} + +// Defines parameters required for initiating and maintaining wireless communication with other devices. +type ModemType struct { + Iccid string `json:"iccid,omitempty" validate:"max=20"` + Imsi string `json:"imsi,omitempty" validate:"max=20"` +} + +// The physical system where an Electrical Vehicle (EV) can be charged. +type ChargingStationType struct { + SerialNumber string `json:"serialNumber,omitempty" validate:"max=25"` + Model string `json:"model" validate:"required,max=20"` + VendorName string `json:"vendorName" validate:"required,max=50"` + FirmwareVersion string `json:"firmwareVersion,omitempty" validate:"max=50"` + Modem *ModemType `json:"modem,omitempty"` +} + +// The field definition of the BootNotification request payload sent by the Charging Station to the CSMS. +type BootNotificationRequest struct { + Reason BootReason `json:"reason" validate:"required,bootReason21"` + ChargingStation ChargingStationType `json:"chargingStation" validate:"required,dive"` +} + +// The field definition of the BootNotification response payload, sent by the CSMS to the Charging Station in response to a BootNotificationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type BootNotificationResponse struct { + CurrentTime *types.DateTime `json:"currentTime" validate:"required"` + Interval int `json:"interval" validate:"gte=0"` + Status RegistrationStatus `json:"status" validate:"required,registrationStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// After each (re)boot, a Charging Station SHALL send a request to the CSMS with information about its configuration (e.g. version, vendor, etc.). +// The CSMS SHALL respond to indicate whether it will accept the Charging Station. +// Between the physical power-on/reboot and the successful completion of a BootNotification, where CSMS returns Accepted or Pending, the Charging Station SHALL NOT send any other request to the CSMS. +// +// When the CSMS responds with a BootNotificationResponse with a status Accepted, the Charging Station will adjust the heartbeat +// interval in accordance with the interval from the response PDU and it is RECOMMENDED to synchronize its internal clock with the supplied CSMS’s current time. +// +// If that interval value is zero, the Charging Station chooses a waiting interval on its own, in a way that avoids flooding the CSMS with requests. +// If the CSMS returns the Pending status, the communication channel SHOULD NOT be closed by either the Charging Station or the CSMS. +// +// The CSMS MAY send request messages to retrieve information from the Charging Station or change its configuration. +type BootNotificationFeature struct{} + +func (f BootNotificationFeature) GetFeatureName() string { + return BootNotificationFeatureName +} + +func (f BootNotificationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(BootNotificationRequest{}) +} + +func (f BootNotificationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(BootNotificationResponse{}) +} + +func (r BootNotificationRequest) GetFeatureName() string { + return BootNotificationFeatureName +} + +func (c BootNotificationResponse) GetFeatureName() string { + return BootNotificationFeatureName +} + +// Creates a new BootNotificationRequest, containing all required fields. Optional fields may be set afterwards. +func NewBootNotificationRequest(reason BootReason, model string, vendorName string) *BootNotificationRequest { + return &BootNotificationRequest{Reason: reason, ChargingStation: ChargingStationType{Model: model, VendorName: vendorName}} +} + +// Creates a new BootNotificationResponse. There are no optional fields for this message. +func NewBootNotificationResponse(currentTime *types.DateTime, interval int, status RegistrationStatus) *BootNotificationResponse { + return &BootNotificationResponse{CurrentTime: currentTime, Interval: interval, Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("registrationStatus21", isValidRegistrationStatus) + _ = types.Validate.RegisterValidation("bootReason21", isValidBootReason) +} diff --git a/ocpp2.1/provisioning/get_base_report.go b/ocpp2.1/provisioning/get_base_report.go new file mode 100644 index 00000000..7edebbf3 --- /dev/null +++ b/ocpp2.1/provisioning/get_base_report.go @@ -0,0 +1,85 @@ +package provisioning + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Get Base Report (CSMS -> CS) -------------------- + +const GetBaseReportFeatureName = "GetBaseReport" + +// Requested availability change in GetBaseReportRequest. +type ReportBaseType string + +const ( + ReportTypeConfigurationInventory ReportBaseType = "ConfigurationInventory" + ReportTypeFullInventory ReportBaseType = "FullInventory" + ReportTypeSummaryInventory ReportBaseType = "SummaryInventory" +) + +func isValidReportBaseType(fl validator.FieldLevel) bool { + status := ReportBaseType(fl.Field().String()) + switch status { + case ReportTypeConfigurationInventory, ReportTypeFullInventory, ReportTypeSummaryInventory: + return true + default: + return false + } +} + +// The field definition of the GetBaseReport request payload sent by the CSMS to the Charging Station. +type GetBaseReportRequest struct { + RequestID int `json:"requestId"` + ReportBase ReportBaseType `json:"reportBase" validate:"required,reportBaseType21"` +} + +// This field definition of the GetBaseReport response payload, sent by the Charging Station to the CSMS in response to a GetBaseReportRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetBaseReportResponse struct { + Status types.GenericDeviceModelStatus `json:"status" validate:"required,genericDeviceModelStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSO may trigger the CSMS to request a report from a Charging Station. +// The CSMS shall then request a Charging Station to send a predefined report as defined in ReportBase. +// The Charging Station responds with GetBaseReportResponse. +// The result will be returned asynchronously in one or more NotifyReportRequest messages (one for each report part). +type GetBaseReportFeature struct{} + +func (f GetBaseReportFeature) GetFeatureName() string { + return GetBaseReportFeatureName +} + +func (f GetBaseReportFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetBaseReportRequest{}) +} + +func (f GetBaseReportFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetBaseReportResponse{}) +} + +func (r GetBaseReportRequest) GetFeatureName() string { + return GetBaseReportFeatureName +} + +func (c GetBaseReportResponse) GetFeatureName() string { + return GetBaseReportFeatureName +} + +// Creates a new GetBaseReportRequest, containing all required fields. There are no optional fields for this message. +func NewGetBaseReportRequest(requestID int, reportBase ReportBaseType) *GetBaseReportRequest { + return &GetBaseReportRequest{RequestID: requestID, ReportBase: reportBase} +} + +// Creates a new GetBaseReportResponse, containing all required fields. There are no optional fields for this message. +func NewGetBaseReportResponse(status types.GenericDeviceModelStatus) *GetBaseReportResponse { + return &GetBaseReportResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("reportBaseType21", isValidReportBaseType) +} diff --git a/ocpp2.1/provisioning/get_report.go b/ocpp2.1/provisioning/get_report.go new file mode 100644 index 00000000..fc2fc0c2 --- /dev/null +++ b/ocpp2.1/provisioning/get_report.go @@ -0,0 +1,85 @@ +package provisioning + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Report (CSMS -> CS) -------------------- + +const GetReportFeatureName = "GetReport" + +// ComponentCriterion indicates the criterion for components requested in GetReportRequest. +type ComponentCriterion string + +const ( + ComponentCriterionActive ComponentCriterion = "Active" + ComponentCriterionAvailable ComponentCriterion = "Available" + ComponentCriterionEnabled ComponentCriterion = "Enabled" + ComponentCriterionProblem ComponentCriterion = "Problem" +) + +func isValidComponentCriterion(fl validator.FieldLevel) bool { + status := ComponentCriterion(fl.Field().String()) + switch status { + case ComponentCriterionActive, ComponentCriterionAvailable, ComponentCriterionEnabled, ComponentCriterionProblem: + return true + default: + return false + } +} + +// The field definition of the GetReport request payload sent by the CSMS to the Charging Station. +type GetReportRequest struct { + RequestID *int `json:"requestId,omitempty" validate:"omitempty,gte=0"` + ComponentCriteria []ComponentCriterion `json:"componentCriteria,omitempty" validate:"omitempty,max=4,dive,componentCriterion21"` + ComponentVariable []types.ComponentVariable `json:"componentVariable,omitempty" validate:"omitempty,dive"` +} + +// This field definition of the GetReport response payload, sent by the Charging Station to the CSMS in response to a GetReportRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetReportResponse struct { + Status types.GenericDeviceModelStatus `json:"status" validate:"required,genericDeviceModelStatus21"` +} + +// The CSO may trigger the CSMS to request a report from a Charging Station. +// The CSMS shall then request a Charging Station to send a report of all Components and Variable limited to those that match ComponentCriteria and/or the list of ComponentVariables. +// The Charging Station responds with GetReportResponse. +// The result will be returned asynchronously in one or more NotifyReportRequest messages (one for each report part). +type GetReportFeature struct{} + +func (f GetReportFeature) GetFeatureName() string { + return GetReportFeatureName +} + +func (f GetReportFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetReportRequest{}) +} + +func (f GetReportFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetReportResponse{}) +} + +func (r GetReportRequest) GetFeatureName() string { + return GetReportFeatureName +} + +func (c GetReportResponse) GetFeatureName() string { + return GetReportFeatureName +} + +// Creates a new GetReportRequest, containing all required fields. Optional fields may be set afterwards. +func NewGetReportRequest() *GetReportRequest { + return &GetReportRequest{} +} + +// Creates a new GetReportResponse, containing all required fields. There are no optional fields for this message. +func NewGetReportResponse(status types.GenericDeviceModelStatus) *GetReportResponse { + return &GetReportResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("componentCriterion21", isValidComponentCriterion) +} diff --git a/ocpp2.1/provisioning/get_variables.go b/ocpp2.1/provisioning/get_variables.go new file mode 100644 index 00000000..64a0e7cc --- /dev/null +++ b/ocpp2.1/provisioning/get_variables.go @@ -0,0 +1,99 @@ +package provisioning + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Variable (CSMS -> CS) -------------------- + +const GetVariablesFeatureName = "GetVariables" + +// GetVariableStatus indicates the result status of getting a variable in GetVariablesResponse. +type GetVariableStatus string + +const ( + GetVariableStatusAccepted GetVariableStatus = "Accepted" + GetVariableStatusRejected GetVariableStatus = "Rejected" + GetVariableStatusUnknownComponent GetVariableStatus = "UnknownComponent" + GetVariableStatusUnknownVariable GetVariableStatus = "UnknownVariable" + GetVariableStatusNotSupported GetVariableStatus = "NotSupportedAttributeType" +) + +func isValidGetVariableStatus(fl validator.FieldLevel) bool { + status := GetVariableStatus(fl.Field().String()) + switch status { + case GetVariableStatusAccepted, GetVariableStatusRejected, GetVariableStatusUnknownComponent, GetVariableStatusUnknownVariable, GetVariableStatusNotSupported: + return true + default: + return false + } +} + +type GetVariableData struct { + AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute21"` + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` +} + +type GetVariableResult struct { + AttributeStatus GetVariableStatus `json:"attributeStatus" validate:"required,getVariableStatus21"` + AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute21"` + AttributeValue string `json:"attributeValue,omitempty" validate:"omitempty,max=1000"` + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` +} + +// The field definition of the GetVariables request payload sent by the CSMS to the Charging Station. +type GetVariablesRequest struct { + GetVariableData []GetVariableData `json:"getVariableData" validate:"required,min=1,dive"` +} + +// This field definition of the GetVariables response payload, sent by the Charging Station to the CSMS in response to a GetVariablesRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetVariablesResponse struct { + GetVariableResult []GetVariableResult `json:"getVariableResult" validate:"required,min=1,dive"` +} + +// The CSO may trigger the CSMS to request to request for a number of variables in a Charging Station. +// The CSMS request the Charging Station for a number of variables (of one or more components) with GetVariablesRequest with a list of requested variables. +// The Charging Station responds with a GetVariablesResponse with the requested variables. +// +// It is not possible to get all attributes of all variables in one call. +type GetVariablesFeature struct{} + +func (f GetVariablesFeature) GetFeatureName() string { + return GetVariablesFeatureName +} + +func (f GetVariablesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetVariablesRequest{}) +} + +func (f GetVariablesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetVariablesResponse{}) +} + +func (r GetVariablesRequest) GetFeatureName() string { + return GetVariablesFeatureName +} + +func (c GetVariablesResponse) GetFeatureName() string { + return GetVariablesFeatureName +} + +// Creates a new GetVariablesRequest, containing all required fields. There are no optional fields for this message. +func NewGetVariablesRequest(variableData []GetVariableData) *GetVariablesRequest { + return &GetVariablesRequest{variableData} +} + +// Creates a new GetVariablesResponse, containing all required fields. There are no optional fields for this message. +func NewGetVariablesResponse(result []GetVariableResult) *GetVariablesResponse { + return &GetVariablesResponse{result} +} + +func init() { + _ = types.Validate.RegisterValidation("getVariableStatus21", isValidGetVariableStatus) +} diff --git a/ocpp2.1/provisioning/notify_report.go b/ocpp2.1/provisioning/notify_report.go new file mode 100644 index 00000000..fcc98bd4 --- /dev/null +++ b/ocpp2.1/provisioning/notify_report.go @@ -0,0 +1,151 @@ +package provisioning + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Notify Report (CS -> CSMS) -------------------- + +const NotifyReportFeatureName = "NotifyReport" + +// Mutability defines the mutability of an attribute. +type Mutability string + +const ( + MutabilityReadOnly Mutability = "ReadOnly" + MutabilityWriteOnly Mutability = "WriteOnly" + MutabilityReadWrite Mutability = "ReadWrite" +) + +func isValidMutability(fl validator.FieldLevel) bool { + status := Mutability(fl.Field().String()) + switch status { + case MutabilityReadOnly, MutabilityWriteOnly, MutabilityReadWrite: + return true + default: + return false + } +} + +// DataType defines the data type of a variable. +type DataType string + +const ( + TypeString DataType = "string" + TypeDecimal DataType = "decimal" + TypeInteger DataType = "integer" + TypeDateTime DataType = "dateTime" + TypeBoolean DataType = "boolean" + TypeOptionList DataType = "OptionList" + TypeSequenceList DataType = "SequenceList" + TypeMemberList DataType = "MemberList" +) + +func isValidDataType(fl validator.FieldLevel) bool { + status := DataType(fl.Field().String()) + switch status { + case TypeBoolean, TypeDateTime, TypeDecimal, TypeInteger, TypeString, TypeOptionList, TypeSequenceList, TypeMemberList: + return true + default: + return false + } +} + +// VariableCharacteristics represents a fixed read-only parameters of a variable. +type VariableCharacteristics struct { + Unit string `json:"unit,omitempty" validate:"max=16"` // Unit of the variable. When the transmitted value has a unit, this field SHALL be included. + DataType DataType `json:"dataType" validate:"required,dataTypeEnum21"` // Data type of this variable. + MinLimit *float64 `json:"minLimit,omitempty"` // Minimum possible value of this variable. + MaxLimit *float64 `json:"maxLimit,omitempty"` // Maximum possible value of this variable. When the datatype of this Variable is String, OptionList, SequenceList or MemberList, this field defines the maximum length of the (CSV) string. + ValuesList string `json:"valuesList,omitempty" validate:"max=1000"` // Allowed values when variable is Option/Member/SequenceList. This is a comma separated list. + SupportsMonitoring bool `json:"supportsMonitoring"` // Flag indicating if this variable supports monitoring. +} + +// NewVariableCharacteristics returns a pointer to a new VariableCharacteristics struct. +func NewVariableCharacteristics(dataType DataType, supportsMonitoring bool) *VariableCharacteristics { + return &VariableCharacteristics{DataType: dataType, SupportsMonitoring: supportsMonitoring} +} + +// VariableAttribute describes the attribute data of a variable. +type VariableAttribute struct { + Type types.Attribute `json:"type,omitempty" validate:"omitempty,attribute21"` // Actual, MinSet, MaxSet, etc. Defaults to Actual if absent. + Value string `json:"value,omitempty" validate:"max=2500"` // Value of the attribute. May only be omitted when mutability is set to 'WriteOnly'. + Mutability Mutability `json:"mutability,omitempty" validate:"omitempty,mutability21"` // Defines the mutability of this attribute. Default is ReadWrite when omitted. + Persistent bool `json:"persistent,omitempty"` // If true, value will be persistent across system reboots or power down. Default when omitted is false. + Constant bool `json:"constant,omitempty"` // If true, value that will never be changed by the Charging Station at runtime. Default when omitted is false. +} + +// NewVariableAttribute creates a VariableAttribute struct, with all default values set. +func NewVariableAttribute() VariableAttribute { + return VariableAttribute{ + Type: types.AttributeActual, + Mutability: MutabilityReadWrite, + } +} + +// ReportData is a struct to report components, variables and variable attributes and characteristics. +type ReportData struct { + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` + VariableAttribute []VariableAttribute `json:"variableAttribute" validate:"required,min=1,max=4,dive"` + VariableCharacteristics *VariableCharacteristics `json:"variableCharacteristics,omitempty" validate:"omitempty"` +} + +// The field definition of the NotifyReport request payload sent by the Charging Station to the CSMS. +type NotifyReportRequest struct { + RequestID int `json:"requestId" validate:"gte=0"` // The id of the GetMonitoringRequest that requested this report. + GeneratedAt *types.DateTime `json:"generatedAt" validate:"required"` // Timestamp of the moment this message was generated at the Charging Station. + Tbc bool `json:"tbc,omitempty" validate:"omitempty"` // “to be continued” indicator. Indicates whether another part of the monitoringData follows in an upcoming notifyMonitoringReportRequest message. Default value when omitted is false. + SeqNo int `json:"seqNo" validate:"gte=0"` // Sequence number of this message. First message starts at 0. + ReportData []ReportData `json:"reportData,omitempty" validate:"omitempty,dive"` // List of ReportData +} + +// The field definition of the NotifyReport response payload, sent by the CSMS to the Charging Station in response to a NotifyReportRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyReportResponse struct { +} + +// A Charging Station may send reports to the CSMS on demand, when requested to do so. +// After receiving a GetBaseReport from the CSMS, a Charging Station asynchronously sends the results +// in one or more NotifyReportRequest messages. +// +// The CSMS responds with NotifyReportResponse for each received request. +type NotifyReportFeature struct{} + +func (f NotifyReportFeature) GetFeatureName() string { + return NotifyReportFeatureName +} + +func (f NotifyReportFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyReportRequest{}) +} + +func (f NotifyReportFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyReportResponse{}) +} + +func (r NotifyReportRequest) GetFeatureName() string { + return NotifyReportFeatureName +} + +func (c NotifyReportResponse) GetFeatureName() string { + return NotifyReportFeatureName +} + +// Creates a new NotifyReportRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyReportRequest(requestID int, generatedAt *types.DateTime, seqNo int) *NotifyReportRequest { + return &NotifyReportRequest{RequestID: requestID, GeneratedAt: generatedAt, SeqNo: seqNo} +} + +// Creates a new NotifyReportResponse. There are no optional fields for this message. +func NewNotifyReportResponse() *NotifyReportResponse { + return &NotifyReportResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("mutability21", isValidMutability) + _ = types.Validate.RegisterValidation("dataTypeEnum21", isValidDataType) +} diff --git a/ocpp2.1/provisioning/provisioning.go b/ocpp2.1/provisioning/provisioning.go new file mode 100644 index 00000000..3bf951cc --- /dev/null +++ b/ocpp2.1/provisioning/provisioning.go @@ -0,0 +1,43 @@ +// The provisioning functional block contains features that help a CSO to provision their Charging Stations, allowing them on their network and retrieving configuration information from these Charging Stations. +// Additionally, it contains features for retrieving information about the configuration of Charging Stations, make changes to the configuration, resetting it etc. +package provisioning + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Provisioning profile. +type CSMSHandler interface { + // OnBootNotification is called on the CSMS whenever a BootNotificationRequest is received from a charging station. + OnBootNotification(chargingStationID string, request *BootNotificationRequest) (response *BootNotificationResponse, err error) + // OnNotifyReport is called on the CSMS whenever a NotifyReportRequest is received from a charging station. + OnNotifyReport(chargingStationID string, request *NotifyReportRequest) (response *NotifyReportResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Provisioning profile. +type ChargingStationHandler interface { + // OnGetBaseReport is called on a charging station whenever a GetBaseReportRequest is received from the CSMS. + OnGetBaseReport(request *GetBaseReportRequest) (response *GetBaseReportResponse, err error) + // OnGetReport is called on a charging station whenever a GetReportRequest is received from the CSMS. + OnGetReport(request *GetReportRequest) (response *GetReportResponse, err error) + // OnGetVariables is called on a charging station whenever a GetVariablesRequest is received from the CSMS. + OnGetVariables(request *GetVariablesRequest) (response *GetVariablesResponse, err error) + // OnReset is called on a charging station whenever a ResetRequest is received from the CSMS. + OnReset(request *ResetRequest) (response *ResetResponse, err error) + // OnSetNetworkProfile is called on a charging station whenever a SetNetworkProfileRequest is received from the CSMS. + OnSetNetworkProfile(request *SetNetworkProfileRequest) (response *SetNetworkProfileResponse, err error) + // OnSetVariables is called on a charging station whenever a SetVariablesRequest is received from the CSMS. + OnSetVariables(request *SetVariablesRequest) (response *SetVariablesResponse, err error) +} + +const ProfileName = "Provisioning" + +var Profile = ocpp.NewProfile( + ProfileName, + BootNotificationFeature{}, + GetBaseReportFeature{}, + GetReportFeature{}, + GetVariablesFeature{}, + NotifyReportFeature{}, + ResetFeature{}, + SetNetworkProfileFeature{}, + SetVariablesFeature{}, +) diff --git a/ocpp2.1/provisioning/reset.go b/ocpp2.1/provisioning/reset.go new file mode 100644 index 00000000..4973aad8 --- /dev/null +++ b/ocpp2.1/provisioning/reset.go @@ -0,0 +1,109 @@ +package provisioning + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Reset (CSMS -> CS) -------------------- + +const ResetFeatureName = "Reset" + +// ResetType indicates the type of reset that the charging station or EVSE should perform, +// as requested by the CSMS in a ResetRequest. +type ResetType string + +const ( + ResetTypeImmediate ResetType = "Immediate" + ResetImmediateAndResume ResetType = "ImmediateAndResume" + ResetTypeOnIdle ResetType = "OnIdle" +) + +func isValidResetType(fl validator.FieldLevel) bool { + status := ResetType(fl.Field().String()) + switch status { + case ResetTypeImmediate, ResetTypeOnIdle, ResetImmediateAndResume: + return true + default: + return false + } +} + +// Result of a ResetRequest. +// This indicates whether the Charging Station is able to perform the reset. +type ResetStatus string + +const ( + ResetStatusAccepted ResetStatus = "Accepted" + ResetStatusRejected ResetStatus = "Rejected" + ResetStatusScheduled ResetStatus = "Scheduled" +) + +func isValidResetStatus(fl validator.FieldLevel) bool { + status := ResetStatus(fl.Field().String()) + switch status { + case ResetStatusAccepted, ResetStatusRejected, ResetStatusScheduled: + return true + default: + return false + } +} + +// The field definition of the Reset request payload sent by the CSMS to the Charging Station. +type ResetRequest struct { + Type ResetType `json:"type" validate:"resetType21"` + 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,resetStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo" validate:"omitempty"` +} + +// The CSO may trigger the CSMS to request a Charging Station to reset itself or an EVSE. +// This can be used when a Charging Station is not functioning correctly, or when the configuration +// (e.g. network, security profiles, etc.) on the Charging Station changed, that needs an explicit reset. +// +// The CSMS sends a ResetRequest to the Charging Station. +// The Charging Station replies with a ResetResponse, then proceeds to reset itself, +// either immediately or whenever possible. +type ResetFeature struct{} + +func (f ResetFeature) GetFeatureName() string { + return ResetFeatureName +} + +func (f ResetFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ResetRequest{}) +} + +func (f ResetFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ResetResponse{}) +} + +func (r ResetRequest) GetFeatureName() string { + return ResetFeatureName +} + +func (c ResetResponse) GetFeatureName() string { + return ResetFeatureName +} + +// Creates a new ResetRequest, containing all required fields. Optional fields may be set afterwards. +func NewResetRequest(t ResetType) *ResetRequest { + return &ResetRequest{Type: t} +} + +// Creates a new ResetResponse, containing all required fields. Optional fields may be set afterwards. +func NewResetResponse(status ResetStatus) *ResetResponse { + return &ResetResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("resetType21", isValidResetType) + _ = types.Validate.RegisterValidation("resetStatus21", isValidResetStatus) +} diff --git a/ocpp2.1/provisioning/set_network_profile.go b/ocpp2.1/provisioning/set_network_profile.go new file mode 100644 index 00000000..5c3523c8 --- /dev/null +++ b/ocpp2.1/provisioning/set_network_profile.go @@ -0,0 +1,226 @@ +package provisioning + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Set Network Profile (CSMS -> CS) -------------------- + +const SetNetworkProfileFeatureName = "SetNetworkProfile" + +// Enumeration of OCPP versions. +type OCPPVersion string + +// OCPP transport mechanisms. SOAP is currently not a valid value for OCPP 2.1 (and is unsupported by this library). +type OCPPTransport string + +// SetNetworkProfileType indicates the type of reset that the charging station or EVSE should perform, +// as requested by the CSMS in a SetNetworkProfileRequest. +type SetNetworkProfileType string + +// Network interface. +type OCPPInterface string + +// Type of VPN. +type VPNType string + +// APN Authentication method. +type APNAuthentication string + +// Result of a SetNetworkProfileRequest. +type SetNetworkProfileStatus string + +const ( + OCPPVersion12 OCPPVersion = "OCPP12" // 1.2 + OCPPVersion15 OCPPVersion = "OCPP15" // 1.5 + OCPPVersion16 OCPPVersion = "OCPP16" // 1.6 + OCPPVersion20 OCPPVersion = "OCPP20" // 2.0 + OCPPVersion21 OCPPVersion = "OCPP21" // 2.1 + + OCPPTransportJSON OCPPTransport = "JSON" // Use JSON over WebSockets for transport of OCPP PDU’s + OCPPTransportSOAP OCPPTransport = "SOAP" // Use SOAP for transport of OCPP PDU’s + + OCPPInterfaceWired0 OCPPInterface = "Wired0" + OCPPInterfaceWired1 OCPPInterface = "Wired1" + OCPPInterfaceWired2 OCPPInterface = "Wired2" + OCPPInterfaceWired3 OCPPInterface = "Wired3" + OCPPInterfaceWireless0 OCPPInterface = "Wireless0" + OCPPInterfaceWireless1 OCPPInterface = "Wireless1" + OCPPInterfaceWireless2 OCPPInterface = "Wireless2" + OCPPInterfaceWireless3 OCPPInterface = "Wireless3" + OCPPInterfaceAny OCPPInterface = "Any" + + VPNTypeIKEv2 VPNType = "IKEv2" + VPNTypeIPSec VPNType = "IPSec" + VPNTypeL2TP VPNType = "L2TP" + VPNTypePPTP VPNType = "PPTP" + + APNAuthenticationCHAP APNAuthentication = "CHAP" + APNAuthenticationNone APNAuthentication = "NONE" + APNAuthenticationPAP APNAuthentication = "PAP" + APNAuthenticationAuto APNAuthentication = "AUTO" // Sequentially try CHAP, PAP, NONE. + + SetNetworkProfileStatusAccepted SetNetworkProfileStatus = "Accepted" + SetNetworkProfileStatusRejected SetNetworkProfileStatus = "Rejected" + SetNetworkProfileStatusFailed SetNetworkProfileStatus = "Failed" +) + +func isValidOCPPVersion(fl validator.FieldLevel) bool { + v := OCPPVersion(fl.Field().String()) + switch v { + case OCPPVersion12, OCPPVersion15, OCPPVersion16, OCPPVersion20: + return true + default: + return false + } +} + +func isValidOCPPTransport(fl validator.FieldLevel) bool { + t := OCPPTransport(fl.Field().String()) + switch t { + case OCPPTransportJSON, OCPPTransportSOAP: + return true + default: + return false + } +} + +func isValidOCPPInterface(fl validator.FieldLevel) bool { + i := OCPPInterface(fl.Field().String()) + switch i { + case OCPPInterfaceWired0, OCPPInterfaceWired1, OCPPInterfaceWired2, OCPPInterfaceWired3, + OCPPInterfaceWireless0, OCPPInterfaceWireless1, OCPPInterfaceWireless2, OCPPInterfaceWireless3, + OCPPInterfaceAny: + return true + default: + return false + } +} + +func isValidVPNType(fl validator.FieldLevel) bool { + t := VPNType(fl.Field().String()) + switch t { + case VPNTypeIKEv2, VPNTypeIPSec, VPNTypeL2TP, VPNTypePPTP: + return true + default: + return false + } +} + +func isValidAPNAuthentication(fl validator.FieldLevel) bool { + a := APNAuthentication(fl.Field().String()) + switch a { + case APNAuthenticationAuto, APNAuthenticationCHAP, APNAuthenticationPAP, APNAuthenticationNone: + return true + default: + return false + } +} + +func isValidSetNetworkProfileStatus(fl validator.FieldLevel) bool { + status := SetNetworkProfileStatus(fl.Field().String()) + switch status { + case SetNetworkProfileStatusAccepted, SetNetworkProfileStatusRejected, SetNetworkProfileStatusFailed: + return true + default: + return false + } +} + +// VPN Configuration settings. +type VPN struct { + Server string `json:"server" validate:"required,max=512"` // VPN Server Address. + User string `json:"user" validate:"required,max=20"` // VPN User. + Group string `json:"group,omitempty" validate:"omitempty,max=20"` // VPN group. + Password string `json:"password" validate:"required,max=20"` // VPN Password. + Key string `json:"key" validate:"required,max=255"` // VPN shared secret. + Type VPNType `json:"type" validate:"required,vpnType21"` // Type of VPN. +} + +type APN struct { + APN string `json:"apn" validate:"required,max=2000"` // The Access Point Name as an URL. + APNUsername string `json:"apnUserName,omitempty" validate:"omitempty,max=50"` // APN username. + APNPassword string `json:"apnPassword,omitempty" validate:"omitempty,max=64"` // APN password. + SimPin *int `json:"simPin,omitempty" validate:"omitempty,gte=0"` // SIM card pin code. + PreferredNetwork string `json:"preferredNetwork,omitempty" validate:"omitempty,max=6"` // Preferred network, written as MCC and MNC concatenated. + UseOnlyPreferredNetwork bool `json:"useOnlyPreferredNetwork,omitempty"` // Use only the preferred Network, do not dial in when not available. + APNAuthentication APNAuthentication `json:"apnAuthentication" validate:"required,apnAuthentication21"` // Authentication method. +} + +// NetworkConnectionProfile defines the functional and technical parameters of a communication link. +type NetworkConnectionProfile struct { + OCPPVersion OCPPVersion `json:"ocppVersion" validate:"required,ocppVersion21"` // The OCPP version used for this communication function. + OCPPTransport OCPPTransport `json:"ocppTransport" validate:"required,ocppTransport21"` // Defines the transport protocol (only OCPP-J is supported by this library). + CSMSUrl string `json:"ocppCsmsUrl" validate:"required,max=512"` // URL of the CSMS(s) that this Charging Station communicates with. + MessageTimeout int `json:"messageTimeout" validate:"gte=-1"` // Duration in seconds before a message send by the Charging Station via this network connection times out. + SecurityProfile int `json:"securityProfile"` // The security profile used when connecting to the CSMS with this NetworkConnectionProfile. + OCPPInterface OCPPInterface `json:"ocppInterface" validate:"required,ocppInterface21"` // Applicable Network Interface. + VPN *VPN `json:"vpn,omitempty" validate:"omitempty"` // Settings to be used to set up the VPN connection. + APN *APN `json:"apn,omitempty" validate:"omitempty"` // Collection of configuration data needed to make a data-connection over a cellular network. + Identity string `json:"identity,omitempty" validate:"omitempty,max=48"` // Charging Station identity to be used as the basic authentication username. + BasicAuthPassword string `json:"basicAuthPassword,omitempty" validate:"omitempty,max=64"` // BasicAuthPassword to use for security profile 1 or 2. +} + +// The field definition of the SetNetworkProfile request payload sent by the CSMS to the Charging Station. +type SetNetworkProfileRequest struct { + ConfigurationSlot int `json:"configurationSlot" validate:"gte=0"` // Slot in which the configuration should be stored. + ConnectionData NetworkConnectionProfile `json:"connectionData" validate:"required"` // Connection details. +} + +// Field definition of the SetNetworkProfile response payload, sent by the Charging Station to the CSMS in response to a SetNetworkProfileRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetNetworkProfileResponse struct { + Status SetNetworkProfileStatus `json:"status" validate:"required,setNetworkProfileStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo" validate:"omitempty"` +} + +// The CSMS may update the connection details on the Charging Station. +// For instance in preparation of a migration to a new CSMS. In order to achieve this, +// the CSMS sends a SetNetworkProfileRequest PDU containing an updated connection profile. +// +// The Charging station validates the content and stores the new data, +// eventually responding with a SetNetworkProfileResponse PDU. +// After completion of this use case, the Charging Station to CSMS connection data has been updated. +type SetNetworkProfileFeature struct{} + +func (f SetNetworkProfileFeature) GetFeatureName() string { + return SetNetworkProfileFeatureName +} + +func (f SetNetworkProfileFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetNetworkProfileRequest{}) +} + +func (f SetNetworkProfileFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetNetworkProfileResponse{}) +} + +func (r SetNetworkProfileRequest) GetFeatureName() string { + return SetNetworkProfileFeatureName +} + +func (c SetNetworkProfileResponse) GetFeatureName() string { + return SetNetworkProfileFeatureName +} + +// Creates a new SetNetworkProfileRequest, containing all required fields. There are no optional fields for this message. +func NewSetNetworkProfileRequest(configurationSlot int, connectionData NetworkConnectionProfile) *SetNetworkProfileRequest { + return &SetNetworkProfileRequest{ConfigurationSlot: configurationSlot, ConnectionData: connectionData} +} + +// Creates a new SetNetworkProfileResponse, containing all required fields. Optional fields may be set afterwards. +func NewSetNetworkProfileResponse(status SetNetworkProfileStatus) *SetNetworkProfileResponse { + return &SetNetworkProfileResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("ocppVersion21", isValidOCPPVersion) + _ = types.Validate.RegisterValidation("ocppTransport", isValidOCPPTransport) + _ = types.Validate.RegisterValidation("ocppInterface21", isValidOCPPInterface) + _ = types.Validate.RegisterValidation("vpnType21", isValidVPNType) + _ = types.Validate.RegisterValidation("apnAuthentication21", isValidAPNAuthentication) + _ = types.Validate.RegisterValidation("setNetworkProfileStatus21", isValidSetNetworkProfileStatus) +} diff --git a/ocpp2.1/provisioning/set_variables.go b/ocpp2.1/provisioning/set_variables.go new file mode 100644 index 00000000..d2c55bbe --- /dev/null +++ b/ocpp2.1/provisioning/set_variables.go @@ -0,0 +1,101 @@ +package provisioning + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Variable (CSMS -> CS) -------------------- + +const SetVariablesFeatureName = "SetVariables" + +// SetVariableStatus indicates the result status of setting a variable in SetVariablesResponse. +type SetVariableStatus string + +const ( + SetVariableStatusAccepted SetVariableStatus = "Accepted" + SetVariableStatusRejected SetVariableStatus = "Rejected" + SetVariableStatusUnknownComponent SetVariableStatus = "UnknownComponent" + SetVariableStatusUnknownVariable SetVariableStatus = "UnknownVariable" + SetVariableStatusNotSupported SetVariableStatus = "NotSupportedAttributeType" + SetVariableStatusRebootRequired SetVariableStatus = "RebootRequired" +) + +func isValidSetVariableStatus(fl validator.FieldLevel) bool { + status := SetVariableStatus(fl.Field().String()) + switch status { + case SetVariableStatusAccepted, SetVariableStatusRejected, SetVariableStatusUnknownComponent, SetVariableStatusUnknownVariable, SetVariableStatusNotSupported, SetVariableStatusRebootRequired: + return true + default: + return false + } +} + +type SetVariableData struct { + AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute21"` + AttributeValue string `json:"attributeValue" validate:"required,max=1000"` + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` +} + +type SetVariableResult struct { + AttributeType types.Attribute `json:"attributeType,omitempty" validate:"omitempty,attribute21"` + AttributeStatus SetVariableStatus `json:"attributeStatus" validate:"required,setVariableStatus21"` + Component types.Component `json:"component" validate:"required"` + Variable types.Variable `json:"variable" validate:"required"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The field definition of the SetVariables request payload sent by the CSMS to the Charging Station. +type SetVariablesRequest struct { + SetVariableData []SetVariableData `json:"setVariableData" validate:"required,min=1,dive"` // List of Component-Variable pairs and attribute values to set. +} + +// This field definition of the SetVariables response payload, sent by the Charging Station to the CSMS in response to a SetVariablesRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetVariablesResponse struct { + SetVariableResult []SetVariableResult `json:"setVariableResult" validate:"required,min=1,dive"` // List of result statuses per Component-Variable. +} + +// A Charging Station can have a lot of variables that can be configured/changed by the CSMS. +// +// The CSO may trigger the CSMS to request setting one or more variables in a Charging Station. +// The CSMS sends a SetVariablesRequest to the Charging Station, to configured/change one or more variables. +// The Charging Station responds with a SetVariablesResponse indicating whether it was able to executed the change(s). +type SetVariablesFeature struct{} + +func (f SetVariablesFeature) GetFeatureName() string { + return SetVariablesFeatureName +} + +func (f SetVariablesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetVariablesRequest{}) +} + +func (f SetVariablesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetVariablesResponse{}) +} + +func (r SetVariablesRequest) GetFeatureName() string { + return SetVariablesFeatureName +} + +func (c SetVariablesResponse) GetFeatureName() string { + return SetVariablesFeatureName +} + +// Creates a new SetVariablesRequest, containing all required fields. There are no optional fields for this message. +func NewSetVariablesRequest(variableData []SetVariableData) *SetVariablesRequest { + return &SetVariablesRequest{variableData} +} + +// Creates a new SetVariablesResponse, containing all required fields. There are no optional fields for this message. +func NewSetVariablesResponse(result []SetVariableResult) *SetVariablesResponse { + return &SetVariablesResponse{result} +} + +func init() { + _ = types.Validate.RegisterValidation("setVariableStatus21", isValidSetVariableStatus) +} diff --git a/ocpp2.1/remotecontrol/remote_control.go b/ocpp2.1/remotecontrol/remote_control.go new file mode 100644 index 00000000..fa33933f --- /dev/null +++ b/ocpp2.1/remotecontrol/remote_control.go @@ -0,0 +1,30 @@ +// The Remote control functional block contains OCPP 2.0 features for remote-control management from the CSMS. +package remotecontrol + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Remote control profile. +type CSMSHandler interface { +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Remote control profile. +type ChargingStationHandler interface { + // OnRequestStartTransaction is called on a charging station whenever a RequestStartTransactionRequest is received from the CSMS. + OnRequestStartTransaction(request *RequestStartTransactionRequest) (response *RequestStartTransactionResponse, err error) + // OnRequestStopTransaction is called on a charging station whenever a RequestStopTransactionRequest is received from the CSMS. + OnRequestStopTransaction(request *RequestStopTransactionRequest) (response *RequestStopTransactionResponse, err error) + // OnTriggerMessage is called on a charging station whenever a TriggerMessageRequest is received from the CSMS. + OnTriggerMessage(request *TriggerMessageRequest) (response *TriggerMessageResponse, err error) + // OnUnlockConnector is called on a charging station whenever a UnlockConnectorRequest is received from the CSMS. + OnUnlockConnector(request *UnlockConnectorRequest) (response *UnlockConnectorResponse, err error) +} + +const ProfileName = "RemoteControl" + +var Profile = ocpp.NewProfile( + ProfileName, + RequestStartTransactionFeature{}, + RequestStopTransactionFeature{}, + TriggerMessageFeature{}, + UnlockConnectorFeature{}, +) diff --git a/ocpp2.1/remotecontrol/request_start_transaction.go b/ocpp2.1/remotecontrol/request_start_transaction.go new file mode 100644 index 00000000..bc616812 --- /dev/null +++ b/ocpp2.1/remotecontrol/request_start_transaction.go @@ -0,0 +1,91 @@ +package remotecontrol + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Request Start Transaction (CSMS -> CS) -------------------- + +const RequestStartTransactionFeatureName = "RequestStartTransaction" + +// Status reported in RequestStartTransactionResponse. +type RequestStartStopStatus string + +const ( + RequestStartStopStatusAccepted RequestStartStopStatus = "Accepted" + RequestStartStopStatusRejected RequestStartStopStatus = "Rejected" +) + +func isValidRequestStartStopStatus(fl validator.FieldLevel) bool { + status := RequestStartStopStatus(fl.Field().String()) + switch status { + case RequestStartStopStatusAccepted, RequestStartStopStatusRejected: + return true + default: + return false + } +} + +// The field definition of the RequestStartTransaction request payload sent by the CSMS to the Charging Station. +type RequestStartTransactionRequest struct { + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gt=0"` + RemoteStartID int `json:"remoteStartId" validate:"gte=0"` + IDToken types.IdToken `json:"idToken"` + ChargingProfile *types.ChargingProfile `json:"chargingProfile,omitempty"` + 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. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type RequestStartTransactionResponse struct { + Status RequestStartStopStatus `json:"status" validate:"required,requestStartStopStatus21"` + TransactionID string `json:"transactionId,omitempty" validate:"max=36"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty"` +} + +// The CSMS may remotely start a transaction for a user. +// This functionality may be triggered by: +// - a CSO, to help out a user, that is having trouble starting a transaction +// - a third-party event (e.g. mobile app) +// - a previously set ChargingProfile +// +// The CSMS sends a RequestStartTransactionRequest to the Charging Station. +// The Charging Stations will reply with a RequestStartTransactionResponse. +type RequestStartTransactionFeature struct{} + +func (f RequestStartTransactionFeature) GetFeatureName() string { + return RequestStartTransactionFeatureName +} + +func (f RequestStartTransactionFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(RequestStartTransactionRequest{}) +} + +func (f RequestStartTransactionFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(RequestStartTransactionResponse{}) +} + +func (r RequestStartTransactionRequest) GetFeatureName() string { + return RequestStartTransactionFeatureName +} + +func (c RequestStartTransactionResponse) GetFeatureName() string { + return RequestStartTransactionFeatureName +} + +// Creates a new RequestStartTransactionRequest, containing all required fields. Optional fields may be set afterwards. +func NewRequestStartTransactionRequest(remoteStartID int, IdToken types.IdToken) *RequestStartTransactionRequest { + return &RequestStartTransactionRequest{RemoteStartID: remoteStartID, IDToken: IdToken} +} + +// Creates a new RequestStartTransactionResponse, containing all required fields. Optional fields may be set afterwards. +func NewRequestStartTransactionResponse(status RequestStartStopStatus) *RequestStartTransactionResponse { + return &RequestStartTransactionResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("requestStartStopStatus21", isValidRequestStartStopStatus) +} diff --git a/ocpp2.1/remotecontrol/request_stop_transaction.go b/ocpp2.1/remotecontrol/request_stop_transaction.go new file mode 100644 index 00000000..65ca86b2 --- /dev/null +++ b/ocpp2.1/remotecontrol/request_stop_transaction.go @@ -0,0 +1,63 @@ +package remotecontrol + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Request Start Transaction (CSMS -> CS) -------------------- + +const RequestStopTransactionFeatureName = "RequestStopTransaction" + +// The field definition of the RequestStopTransaction request payload sent by the CSMS to the Charging Station. +type RequestStopTransactionRequest struct { + TransactionID string `json:"transactionId" validate:"required,max=36"` +} + +// This field definition of the RequestStopTransaction response payload, sent by the Charging Station to the CSMS in response to a RequestStopTransactionRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type RequestStopTransactionResponse struct { + Status RequestStartStopStatus `json:"status" validate:"required,requestStartStopStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty"` +} + +// The CSMS may remotely stop an ongoing transaction for a user. +// This functionality may be triggered by: +// - a CSO, to help out a user, that is having trouble stopping a transaction +// - a third-party event (e.g. mobile app) +// - the ISO15118-1 use-case F2 +// +// The CSMS sends a RequestStopTransactionRequest to the Charging Station. +// The Charging Stations will reply with a RequestStopTransactionResponse. +type RequestStopTransactionFeature struct{} + +func (f RequestStopTransactionFeature) GetFeatureName() string { + return RequestStopTransactionFeatureName +} + +func (f RequestStopTransactionFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(RequestStopTransactionRequest{}) +} + +func (f RequestStopTransactionFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(RequestStopTransactionResponse{}) +} + +func (r RequestStopTransactionRequest) GetFeatureName() string { + return RequestStopTransactionFeatureName +} + +func (c RequestStopTransactionResponse) GetFeatureName() string { + return RequestStopTransactionFeatureName +} + +// Creates a new RequestStopTransactionRequest, containing all required fields. There are no optional fields for this message. +func NewRequestStopTransactionRequest(transactionID string) *RequestStopTransactionRequest { + return &RequestStopTransactionRequest{TransactionID: transactionID} +} + +// Creates a new RequestStopTransactionResponse, containing all required fields. Optional fields may be set afterwards. +func NewRequestStopTransactionResponse(status RequestStartStopStatus) *RequestStopTransactionResponse { + return &RequestStopTransactionResponse{Status: status} +} diff --git a/ocpp2.1/remotecontrol/trigger_message.go b/ocpp2.1/remotecontrol/trigger_message.go new file mode 100644 index 00000000..28929bf8 --- /dev/null +++ b/ocpp2.1/remotecontrol/trigger_message.go @@ -0,0 +1,116 @@ +package remotecontrol + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Trigger Message (CSMS -> CS) -------------------- + +const TriggerMessageFeatureName = "TriggerMessage" + +// Type of request to be triggered by trigger messages. +type MessageTrigger string + +// Status in TriggerMessageResponse. +type TriggerMessageStatus string + +const ( + MessageTriggerBootNotification MessageTrigger = "BootNotification" + MessageTriggerLogStatusNotification MessageTrigger = "LogStatusNotification" + MessageTriggerFirmwareStatusNotification MessageTrigger = "FirmwareStatusNotification" + MessageTriggerHeartbeat MessageTrigger = "Heartbeat" + MessageTriggerMeterValues MessageTrigger = "MeterValues" + MessageTriggerSignChargingStationCertificate MessageTrigger = "SignChargingStationCertificate" + MessageTriggerSignV2GCertificate MessageTrigger = "SignV2GCertificate" + MessageTriggerStatusNotification MessageTrigger = "StatusNotification" + MessageTriggerTransactionEvent MessageTrigger = "TransactionEvent" + MessageTriggerSignCombinedCertificate MessageTrigger = "SignCombinedCertificate" + MessageTriggerPublishFirmwareStatusNotification MessageTrigger = "PublishFirmwareStatusNotification" + MessageTriggerSignV2G20Certificate MessageTrigger = "SignV2G20Certificate" + MessageTriggerCustomTrigger MessageTrigger = "CustomTrigger" + + TriggerMessageStatusAccepted TriggerMessageStatus = "Accepted" + TriggerMessageStatusRejected TriggerMessageStatus = "Rejected" + TriggerMessageStatusNotImplemented TriggerMessageStatus = "NotImplemented" +) + +func isValidMessageTrigger(fl validator.FieldLevel) bool { + status := MessageTrigger(fl.Field().String()) + switch status { + case MessageTriggerBootNotification, MessageTriggerLogStatusNotification, MessageTriggerFirmwareStatusNotification, + MessageTriggerHeartbeat, MessageTriggerMeterValues, MessageTriggerSignChargingStationCertificate, + MessageTriggerSignV2GCertificate, MessageTriggerStatusNotification, MessageTriggerTransactionEvent, + MessageTriggerSignCombinedCertificate, MessageTriggerPublishFirmwareStatusNotification, + MessageTriggerSignV2G20Certificate, MessageTriggerCustomTrigger: + return true + default: + return false + } +} + +func isValidTriggerMessageStatus(fl validator.FieldLevel) bool { + status := TriggerMessageStatus(fl.Field().String()) + switch status { + case TriggerMessageStatusAccepted, TriggerMessageStatusRejected, TriggerMessageStatusNotImplemented: + return true + default: + return false + } +} + +// The field definition of the TriggerMessage request payload sent by the CSMS to the Charging Station. +type TriggerMessageRequest struct { + RequestedMessage MessageTrigger `json:"requestedMessage" validate:"required,messageTrigger21"` + CustomTrigger string `json:"customTrigger,omitempty" validate:"omitempty,max=50"` + Evse *types.EVSE `json:"evse,omitempty" validate:"omitempty"` +} + +// This field definition of the TriggerMessage response payload, sent by the Charging Station to the CSMS in response to a TriggerMessageRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type TriggerMessageResponse struct { + Status TriggerMessageStatus `json:"status" validate:"required,triggerMessageStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty"` +} + +// The CSMS may request a Charging Station to send a Charging Station-initiated message. +// This is achieved sending a TriggerMessageRequest to a charging station, indicating which message should be received. +// The charging station responds with a TriggerMessageResponse, indicating whether it will send a message or not. +type TriggerMessageFeature struct{} + +func (f TriggerMessageFeature) GetFeatureName() string { + return TriggerMessageFeatureName +} + +func (f TriggerMessageFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(TriggerMessageRequest{}) +} + +func (f TriggerMessageFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(TriggerMessageResponse{}) +} + +func (r TriggerMessageRequest) GetFeatureName() string { + return TriggerMessageFeatureName +} + +func (c TriggerMessageResponse) GetFeatureName() string { + return TriggerMessageFeatureName +} + +// Creates a new TriggerMessageRequest, containing all required fields. Optional fields may be set afterwards. +func NewTriggerMessageRequest(requestedMessage MessageTrigger) *TriggerMessageRequest { + return &TriggerMessageRequest{RequestedMessage: requestedMessage} +} + +// Creates a new TriggerMessageResponse, containing all required fields. Optional fields may be set afterwards. +func NewTriggerMessageResponse(status TriggerMessageStatus) *TriggerMessageResponse { + return &TriggerMessageResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("messageTrigger21", isValidMessageTrigger) + _ = types.Validate.RegisterValidation("triggerMessageStatus21", isValidTriggerMessageStatus) +} diff --git a/ocpp2.1/remotecontrol/unlock_connector.go b/ocpp2.1/remotecontrol/unlock_connector.go new file mode 100644 index 00000000..0c6802a3 --- /dev/null +++ b/ocpp2.1/remotecontrol/unlock_connector.go @@ -0,0 +1,90 @@ +package remotecontrol + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Trigger Message (CSMS -> CS) -------------------- + +const UnlockConnectorFeatureName = "UnlockConnector" + +// Status in UnlockConnectorResponse. +type UnlockStatus string + +const ( + UnlockStatusUnlocked UnlockStatus = "Unlocked" // Connector has successfully been unlocked. + UnlockStatusUnlockFailed UnlockStatus = "UnlockFailed" // Failed to unlock the connector. + UnlockStatusOngoingAuthorizedTransaction UnlockStatus = "OngoingAuthorizedTransaction" // The connector is not unlocked, because there is still an authorized transaction ongoing. + UnlockStatusUnknownConnector UnlockStatus = "UnknownConnector" // The specified connector is not known by the Charging Station. +) + +func isValidUnlockStatus(fl validator.FieldLevel) bool { + status := UnlockStatus(fl.Field().String()) + switch status { + case UnlockStatusUnlocked, + UnlockStatusUnlockFailed, + UnlockStatusOngoingAuthorizedTransaction, + UnlockStatusUnknownConnector: + return true + default: + return false + } +} + +// The field definition of the UnlockConnector request payload sent by the CSMS to the Charging Station. +type UnlockConnectorRequest struct { + EvseID int `json:"evseId" validate:"gte=0"` + ConnectorID int `json:"connectorId" validate:"gte=0"` +} + +// This field definition of the UnlockConnector response payload, sent by the Charging Station to the CSMS in response to a UnlockConnectorRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type UnlockConnectorResponse struct { + Status UnlockStatus `json:"status" validate:"required,unlockStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty"` +} + +// It sometimes happens that a connector of a Charging Station socket does not unlock correctly. +// This happens most of the time when there is tension on the charging cable. +// This means the driver cannot unplug his charging cable from the Charging Station. +// To help a driver, the CSO can send a UnlockConnectorRequest to the Charging Station. +// The Charging Station will then try to unlock the connector again and respond with an UnlockConnectorResponse. +type UnlockConnectorFeature struct{} + +func (f UnlockConnectorFeature) GetFeatureName() string { + return UnlockConnectorFeatureName +} + +func (f UnlockConnectorFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(UnlockConnectorRequest{}) +} + +func (f UnlockConnectorFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(UnlockConnectorResponse{}) +} + +func (r UnlockConnectorRequest) GetFeatureName() string { + return UnlockConnectorFeatureName +} + +func (c UnlockConnectorResponse) GetFeatureName() string { + return UnlockConnectorFeatureName +} + +// Creates a new UnlockConnectorRequest, containing all required fields. There are no optional fields for this message. +func NewUnlockConnectorRequest(evseID int, connectorID int) *UnlockConnectorRequest { + return &UnlockConnectorRequest{EvseID: evseID, ConnectorID: connectorID} +} + +// Creates a new UnlockConnectorResponse, containing all required fields. Optional fields may be set afterwards. +func NewUnlockConnectorResponse(status UnlockStatus) *UnlockConnectorResponse { + return &UnlockConnectorResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("unlockStatus21", isValidUnlockStatus) +} diff --git a/ocpp2.1/reservation/cancel_reservation.go b/ocpp2.1/reservation/cancel_reservation.go new file mode 100644 index 00000000..860276e0 --- /dev/null +++ b/ocpp2.1/reservation/cancel_reservation.go @@ -0,0 +1,82 @@ +package reservation + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Cancel Reservation (CSMS -> CS) -------------------- + +const CancelReservationFeatureName = "CancelReservation" + +// Status reported in CancelReservationResponse. +type CancelReservationStatus string + +const ( + CancelReservationStatusAccepted CancelReservationStatus = "Accepted" + CancelReservationStatusRejected CancelReservationStatus = "Rejected" +) + +func isValidCancelReservationStatus(fl validator.FieldLevel) bool { + status := CancelReservationStatus(fl.Field().String()) + switch status { + case CancelReservationStatusAccepted, CancelReservationStatusRejected: + return true + default: + return false + } +} + +// The field definition of the CancelReservation request payload sent by the CSMS to the Charging Station. +type CancelReservationRequest struct { + ReservationID int `json:"reservationId" validate:"gte=0"` +} + +// This field definition of the CancelReservation response payload, sent by the Charging Station to the CSMS in response to a CancelReservationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type CancelReservationResponse struct { + Status CancelReservationStatus `json:"status" validate:"required,cancelReservationStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// To cancel a reservation the CSMS SHALL send an CancelReservationRequest to the Charging Station. +// If the Charging Station has a reservation matching the reservationId in the request payload, it SHALL return status ‘Accepted’. +// Otherwise it SHALL return ‘Rejected’. +type CancelReservationFeature struct{} + +func (f CancelReservationFeature) GetFeatureName() string { + return CancelReservationFeatureName +} + +func (f CancelReservationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(CancelReservationRequest{}) +} + +func (f CancelReservationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(CancelReservationResponse{}) +} + +func (r CancelReservationRequest) GetFeatureName() string { + return CancelReservationFeatureName +} + +func (c CancelReservationResponse) GetFeatureName() string { + return CancelReservationFeatureName +} + +// Creates a new CancelReservationRequest, containing all required fields. There are no optional fields for this message. +func NewCancelReservationRequest(reservationId int) *CancelReservationRequest { + return &CancelReservationRequest{ReservationID: reservationId} +} + +// Creates a new CancelReservationResponse, containing all required fields. There are no optional fields for this message. +func NewCancelReservationResponse(status CancelReservationStatus) *CancelReservationResponse { + return &CancelReservationResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("cancelReservationStatus21", isValidCancelReservationStatus) +} diff --git a/ocpp2.1/reservation/reservation.go b/ocpp2.1/reservation/reservation.go new file mode 100644 index 00000000..63cfd4ac --- /dev/null +++ b/ocpp2.1/reservation/reservation.go @@ -0,0 +1,29 @@ +// The reservation functional block contains OCPP 2.0 features that enable EV drivers to make and manage reservations of charging stations. +package reservation + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" +) + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Reservation profile. +type CSMSHandler interface { + // OnReservationStatusUpdate is called on the CSMS whenever a ReservationStatusUpdateRequest is received from a charging station. + OnReservationStatusUpdate(chargingStationID string, request *ReservationStatusUpdateRequest) (resp *ReservationStatusUpdateResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Reservation profile. +type ChargingStationHandler interface { + // OnCancelReservation is called on a charging station whenever a CancelReservationRequest is received from the CSMS. + OnCancelReservation(request *CancelReservationRequest) (resp *CancelReservationResponse, err error) + // OnReserveNow is called on a charging station whenever a ReserveNowRequest is received from the CSMS. + OnReserveNow(request *ReserveNowRequest) (resp *ReserveNowResponse, err error) +} + +const ProfileName = "Reservation" + +var Profile = ocpp.NewProfile( + ProfileName, + CancelReservationFeature{}, + ReservationStatusUpdateFeature{}, + ReserveNowFeature{}, +) diff --git a/ocpp2.1/reservation/reservation_status_update.go b/ocpp2.1/reservation/reservation_status_update.go new file mode 100644 index 00000000..d5f2ee5d --- /dev/null +++ b/ocpp2.1/reservation/reservation_status_update.go @@ -0,0 +1,85 @@ +package reservation + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Reservation Status Update (CS -> CSMS) -------------------- + +const ReservationStatusUpdateFeatureName = "ReservationStatusUpdate" + +// Status reported in ReservationStatusUpdateRequest. +type ReservationUpdateStatus string + +const ( + ReservationUpdateStatusExpired ReservationUpdateStatus = "Expired" + ReservationUpdateStatusRemoved ReservationUpdateStatus = "Removed" +) + +func isValidReservationUpdateStatus(fl validator.FieldLevel) bool { + status := ReservationUpdateStatus(fl.Field().String()) + switch status { + case ReservationUpdateStatusExpired, ReservationUpdateStatusRemoved: + return true + default: + return false + } +} + +// The field definition of the ReservationStatusUpdate request payload sent by the Charging Station to the CSMS. +type ReservationStatusUpdateRequest struct { + ReservationID int `json:"reservationId" validate:"gte=0"` + Status ReservationUpdateStatus `json:"reservationUpdateStatus" validate:"required,reservationUpdateStatus21"` +} + +// This field definition of the ReservationStatusUpdate response payload, sent by the CSMS to the Charging Station in response to a ReservationStatusUpdateRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ReservationStatusUpdateResponse struct { +} + +// A Charging Station shall cancel an existing reservation when: +// - the status of a targeted EVSE changes to either Faulted or Unavailable +// - the reservation has expired, before the EV driver started using the Charging Station +// +// This message is not triggered, if a reservation is explicitly canceled by the user or the CSMS. +// +// The Charging Station sends a ReservationStatusUpdateRequest to the CSMS, with the according status set. +// The CSMS responds with a ReservationStatusUpdateResponse. +type ReservationStatusUpdateFeature struct{} + +func (f ReservationStatusUpdateFeature) GetFeatureName() string { + return ReservationStatusUpdateFeatureName +} + +func (f ReservationStatusUpdateFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ReservationStatusUpdateRequest{}) +} + +func (f ReservationStatusUpdateFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ReservationStatusUpdateResponse{}) +} + +func (r ReservationStatusUpdateRequest) GetFeatureName() string { + return ReservationStatusUpdateFeatureName +} + +func (c ReservationStatusUpdateResponse) GetFeatureName() string { + return ReservationStatusUpdateFeatureName +} + +// Creates a new ReservationStatusUpdateRequest, containing all required fields. There are no optional fields for this message. +func NewReservationStatusUpdateRequest(reservationID int, status ReservationUpdateStatus) *ReservationStatusUpdateRequest { + return &ReservationStatusUpdateRequest{ReservationID: reservationID, Status: status} +} + +// Creates a new ReservationStatusUpdateResponse, which doesn't contain any required or optional fields. +func NewReservationStatusUpdateResponse() *ReservationStatusUpdateResponse { + return &ReservationStatusUpdateResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("reservationUpdateStatus21", isValidReservationUpdateStatus) +} diff --git a/ocpp2.1/reservation/reserve_now.go b/ocpp2.1/reservation/reserve_now.go new file mode 100644 index 00000000..a06d21bb --- /dev/null +++ b/ocpp2.1/reservation/reserve_now.go @@ -0,0 +1,140 @@ +package reservation + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Reserve Now (CSMS -> CS) -------------------- + +const ReserveNowFeatureName = "ReserveNow" + +// Status reported in ReserveNowResponse. +type ReserveNowStatus string + +const ( + ReserveNowStatusAccepted ReserveNowStatus = "Accepted" + ReserveNowStatusFaulted ReserveNowStatus = "Faulted" + ReserveNowStatusOccupied ReserveNowStatus = "Occupied" + ReserveNowStatusRejected ReserveNowStatus = "Rejected" + ReserveNowStatusUnavailable ReserveNowStatus = "Unavailable" +) + +func isValidReserveNowStatus(fl validator.FieldLevel) bool { + status := ReserveNowStatus(fl.Field().String()) + switch status { + case ReserveNowStatusAccepted, ReserveNowStatusFaulted, ReserveNowStatusOccupied, ReserveNowStatusRejected, ReserveNowStatusUnavailable: + return true + default: + return false + } +} + +// Allowed ConnectorType, as supported by most charging station vendors. +// The OCPP protocol directly supports the most widely known connector types. For not mentioned types, +// refer to the Other1PhMax16A, Other1PhOver16A and Other3Ph fallbacks. +type ConnectorType string + +const ( + ConnectorTypeCCS1 ConnectorType = "cCCS1" // Combined Charging System 1 (captive cabled) a.k.a. Combo 1 + ConnectorTypeCCS2 ConnectorType = "cCCS2" // Combined Charging System 2 (captive cabled) a.k.a. Combo 2 + ConnectorTypeG105 ConnectorType = "cG105" // JARI G105-1993 (captive cabled) a.k.a. CHAdeMO + ConnectorTypeTesla ConnectorType = "cTesla" // Tesla Connector + ConnectorTypeCType1 ConnectorType = "cType1" // IEC62196-2 Type 1 connector (captive cabled) a.k.a. J1772 + ConnectorTypeCType2 ConnectorType = "cType2" // IEC62196-2 Type 2 connector (captive cabled) a.k.a. Mennekes connector + ConnectorType3091P16A ConnectorType = "s309-1P-16A" // 16A 1 phase IEC60309 socket + ConnectorType3091P32A ConnectorType = "s309-1P-32A" // 32A 1 phase IEC60309 socket + ConnectorType3093P16A ConnectorType = "s309-3P-16A" // 16A 3 phase IEC60309 socket + ConnectorType3093P32A ConnectorType = "s309-3P-32A" // 32A 3 phase IEC60309 socket + ConnectorTypeBS1361 ConnectorType = "sBS1361" // UK domestic socket a.k.a. 13Amp + ConnectorTypeCEE77 ConnectorType = "sCEE-7-7" // CEE 7/7 16A socket. May represent 7/4 & 7/5 a.k.a Schuko + ConnectorTypeSType2 ConnectorType = "sType2" // EC62196-2 Type 2 socket a.k.a. Mennekes connector + ConnectorTypeSType3 ConnectorType = "sType3" // IEC62196-2 Type 2 socket a.k.a. Scame + ConnectorTypeOther1PhMax16A ConnectorType = "Other1PhMax16A" // Other single phase (domestic) sockets not mentioned above, rated at no more than 16A. CEE7/17, AS3112, NEMA 5-15, NEMA 5-20, JISC8303, TIS166, SI 32, CPCS-CCC, SEV1011, etc. + ConnectorTypeOther1PhOver16A ConnectorType = "Other1PhOver16A" // Other single phase sockets not mentioned above (over 16A) + ConnectorTypeOther3Ph ConnectorType = "Other3Ph" // Other 3 phase sockets not mentioned above. NEMA14-30, NEMA14-50. + ConnectorTypePan ConnectorType = "Pan" // Pantograph connector + ConnectorTypeWirelessInductive ConnectorType = "wInductive" // Wireless inductively coupled connection + ConnectorTypeWirelessResonant ConnectorType = "wResonant" // Wireless resonant coupled connection + ConnectorTypeUndetermined ConnectorType = "Undetermined" // Yet to be determined (e.g. before plugged in) + ConnectorTypeUnknown ConnectorType = "Unknown" // Unknown; not determinable +) + +func isValidConnectorType(fl validator.FieldLevel) bool { + status := ConnectorType(fl.Field().String()) + switch status { + case ConnectorTypeCCS1, ConnectorTypeCCS2, ConnectorTypeG105, ConnectorTypeTesla, ConnectorTypeCType1, + ConnectorTypeCType2, ConnectorType3091P16A, ConnectorType3091P32A, ConnectorType3093P16A, ConnectorType3093P32A, + ConnectorTypeBS1361, ConnectorTypeCEE77, ConnectorTypeSType2, ConnectorTypeSType3, ConnectorTypeOther1PhMax16A, + ConnectorTypeOther1PhOver16A, ConnectorTypeOther3Ph, ConnectorTypePan, ConnectorTypeWirelessInductive, + ConnectorTypeWirelessResonant, ConnectorTypeUndetermined, ConnectorTypeUnknown: + return true + default: + return false + } +} + +// 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,connectorType21"` + 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. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ReserveNowResponse struct { + Status ReserveNowStatus `json:"status" validate:"required,reserveNowStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// To ensure an EV drive can charge their EV at a charging station, the EV driver may make a reservation until +// a certain expiry time. A user may reserve a specific EVSE. +// +// The EV driver asks the CSMS to reserve an unspecified EVSE at a charging station. +// The CSMS sends a ReserveNowRequest to a charging station. +// The charging station responds with ReserveNowResponse, with an according status. +// +// After confirming a reservation, the charging station shall asynchronously send a +// StatusNotificationRequest to the CSMS. +type ReserveNowFeature struct{} + +func (f ReserveNowFeature) GetFeatureName() string { + return ReserveNowFeatureName +} + +func (f ReserveNowFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ReserveNowRequest{}) +} + +func (f ReserveNowFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ReserveNowResponse{}) +} + +func (r ReserveNowRequest) GetFeatureName() string { + return ReserveNowFeatureName +} + +func (c ReserveNowResponse) GetFeatureName() string { + return ReserveNowFeatureName +} + +// Creates a new ReserveNowRequest, containing all required fields. Optional fields may be set afterwards. +func NewReserveNowRequest(id int, expiryDateTime *types.DateTime, idToken types.IdToken) *ReserveNowRequest { + return &ReserveNowRequest{ID: id, ExpiryDateTime: expiryDateTime, IdToken: idToken} +} + +// Creates a new ReserveNowResponse, containing all required fields. Optional fields may be set afterwards. +func NewReserveNowResponse(status ReserveNowStatus) *ReserveNowResponse { + return &ReserveNowResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("reserveNowStatus21", isValidReserveNowStatus) + _ = types.Validate.RegisterValidation("connectorType21", isValidConnectorType) +} diff --git a/ocpp2.1/security/certificate_signed.go b/ocpp2.1/security/certificate_signed.go new file mode 100644 index 00000000..571ae522 --- /dev/null +++ b/ocpp2.1/security/certificate_signed.go @@ -0,0 +1,84 @@ +package security + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Certificate Signed (CSMS -> CS) -------------------- + +const CertificateSignedFeatureName = "CertificateSigned" + +// Status returned in response to CertificateSignedRequest, that indicates whether certificate signing has been accepted or rejected. +type CertificateSignedStatus string + +const ( + CertificateSignedStatusAccepted CertificateSignedStatus = "Accepted" + CertificateSignedStatusRejected CertificateSignedStatus = "Rejected" +) + +func isValidCertificateSignedStatus(fl validator.FieldLevel) bool { + status := CertificateSignedStatus(fl.Field().String()) + switch status { + case CertificateSignedStatusAccepted, CertificateSignedStatusRejected: + return true + default: + return false + } +} + +// 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:"certificateType,omitempty" validate:"omitempty,certificateSigningUse21"` + RequestId *int `json:"requestId,omitempty" validate:"omitempty"` +} + +// The field definition of the CertificateSignedResponse payload sent by the Charging Station to the CSMS in response to a CertificateSignedRequest. +type CertificateSignedResponse struct { + Status CertificateSignedStatus `json:"status" validate:"required,certificateSignedStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// During the a certificate update procedure, the CSMS sends a new certificate, signed by a CA, +// to the Charging Station with a CertificateSignedRequest. +// The Charging Station verifies the signed certificate, installs it locally and responds with +// a CertificateSignedResponse to the the CSMS with the status Accepted or Rejected. +type CertificateSignedFeature struct{} + +func (f CertificateSignedFeature) GetFeatureName() string { + return CertificateSignedFeatureName +} + +func (f CertificateSignedFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(CertificateSignedRequest{}) +} + +func (f CertificateSignedFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(CertificateSignedResponse{}) +} + +func (r CertificateSignedRequest) GetFeatureName() string { + return CertificateSignedFeatureName +} + +func (c CertificateSignedResponse) GetFeatureName() string { + return CertificateSignedFeatureName +} + +// Creates a new CertificateSignedRequest, containing all required fields. Additional optional fields may be set afterwards. +func NewCertificateSignedRequest(certificateChain string) *CertificateSignedRequest { + return &CertificateSignedRequest{CertificateChain: certificateChain} +} + +// Creates a new CertificateSignedResponse, containing all required fields. There are no optional fields for this message. +func NewCertificateSignedResponse(status CertificateSignedStatus) *CertificateSignedResponse { + return &CertificateSignedResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("certificateSignedStatus21", isValidCertificateSignedStatus) +} diff --git a/ocpp2.1/security/security.go b/ocpp2.1/security/security.go new file mode 100644 index 00000000..a3c23a0b --- /dev/null +++ b/ocpp2.1/security/security.go @@ -0,0 +1,27 @@ +// The security functional block contains OCPP 2.0 features aimed at providing E2E security between a CSMS and a Charging station. +package security + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Security profile. +type CSMSHandler interface { + // OnSecurityEventNotification is called on the CSMS whenever a SecurityEventNotificationRequest is received from a charging station. + OnSecurityEventNotification(chargingStationID string, request *SecurityEventNotificationRequest) (response *SecurityEventNotificationResponse, err error) + // OnSignCertificate is called on the CSMS whenever a SignCertificateRequest is received from a charging station. + OnSignCertificate(chargingStationID string, request *SignCertificateRequest) (response *SignCertificateResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Security profile. +type ChargingStationHandler interface { + // OnCertificateSigned is called on a charging station whenever a CertificateSignedRequest is received from the CSMS. + OnCertificateSigned(request *CertificateSignedRequest) (response *CertificateSignedResponse, err error) +} + +const ProfileName = "Security" + +var Profile = ocpp.NewProfile( + ProfileName, + CertificateSignedFeature{}, + SecurityEventNotificationFeature{}, + SignCertificateFeature{}, +) diff --git a/ocpp2.1/security/security_event_notification.go b/ocpp2.1/security/security_event_notification.go new file mode 100644 index 00000000..758c88f5 --- /dev/null +++ b/ocpp2.1/security/security_event_notification.go @@ -0,0 +1,58 @@ +package security + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Security Event Notification Status (CS -> CSMS) -------------------- + +const SecurityEventNotificationFeatureName = "SecurityEventNotification" + +// The field definition of the SecurityEventNotification request payload sent by the Charging Station to the CSMS. +type SecurityEventNotificationRequest struct { + Type string `json:"type" validate:"required,max=50"` // Type of the security event. This value should be taken from the Security events list. + Timestamp *types.DateTime `json:"timestamp" validate:"required"` // Date and time at which the event occurred. + TechInfo string `json:"techInfo,omitempty" validate:"omitempty,max=255"` // Additional information about the occurred security event. +} + +// This field definition of the SecurityEventNotification response payload, sent by the CSMS to the Charging Station in response to a SecurityEventNotificationRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SecurityEventNotificationResponse struct { +} + +// In case of critical security events, a Charging Station may immediately inform the CSMS of such events, +// via a SecurityEventNotificationRequest. +// The CSMS responds with a SecurityEventNotificationResponse to the Charging Station. +type SecurityEventNotificationFeature struct{} + +func (f SecurityEventNotificationFeature) GetFeatureName() string { + return SecurityEventNotificationFeatureName +} + +func (f SecurityEventNotificationFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SecurityEventNotificationRequest{}) +} + +func (f SecurityEventNotificationFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SecurityEventNotificationResponse{}) +} + +func (r SecurityEventNotificationRequest) GetFeatureName() string { + return SecurityEventNotificationFeatureName +} + +func (c SecurityEventNotificationResponse) GetFeatureName() string { + return SecurityEventNotificationFeatureName +} + +// Creates a new SecurityEventNotificationRequest, containing all required fields. Optional fields may be set afterwards. +func NewSecurityEventNotificationRequest(typ string, timestamp *types.DateTime) *SecurityEventNotificationRequest { + return &SecurityEventNotificationRequest{Type: typ, Timestamp: timestamp} +} + +// Creates a new SecurityEventNotificationResponse, which doesn't contain any required or optional fields. +func NewSecurityEventNotificationResponse() *SecurityEventNotificationResponse { + return &SecurityEventNotificationResponse{} +} diff --git a/ocpp2.1/security/sign_certificate.go b/ocpp2.1/security/sign_certificate.go new file mode 100644 index 00000000..d5e3494d --- /dev/null +++ b/ocpp2.1/security/sign_certificate.go @@ -0,0 +1,64 @@ +package security + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Sign Certificate (CS -> CSMS) -------------------- + +const SignCertificateFeatureName = "SignCertificate" + +// The field definition of the SignCertificate request payload sent by the Charging Station to the CSMS. +type SignCertificateRequest struct { + CSR string `json:"csr" validate:"required,max=5500"` // The Charging Station SHALL send the public key in form of a Certificate Signing Request (CSR) as described in RFC 2986 and then PEM encoded. + CertificateType types.CertificateSigningUse `json:"certificateType,omitempty" validate:"omitempty,certificateSigningUse21"` // Indicates the type of certificate that is to be signed. + RequestId *int `json:"requestId,omitempty"` // RequestId to match this message with the CertificateSignedRequest + HashRootCertificate types.CertificateHashData `json:"hashRootCertificate,omitempty" validate:"omitempty,certificateHashData21"` // The hash of the root certificate to identify the PKI to use. +} + +// This field definition of the SignCertificate response payload, sent by the CSMS to the Charging Station in response to a SignCertificateRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SignCertificateResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` // Specifies whether the CSMS can process the request. + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` // Detailed status information. +} + +// If a Charging Station detected, that its certificate is due to expire, it will generate a new public/private key pair, +// then send a SignCertificateRequest to the CSMS containing a valid Certificate Signing Request. +// +// The CSMS responds with a SignCertificateResponse and will then forward the CSR to a CA server. +// Once the CA has issues a valid certificate, the CSMS will send a CertificateSignedRequest to the +// charging station (asynchronously). +type SignCertificateFeature struct{} + +func (f SignCertificateFeature) GetFeatureName() string { + return SignCertificateFeatureName +} + +func (f SignCertificateFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SignCertificateRequest{}) +} + +func (f SignCertificateFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SignCertificateResponse{}) +} + +func (r SignCertificateRequest) GetFeatureName() string { + return SignCertificateFeatureName +} + +func (c SignCertificateResponse) GetFeatureName() string { + return SignCertificateFeatureName +} + +// Creates a new SignCertificateRequest, containing all required fields. Optional fields may be set afterwards. +func NewSignCertificateRequest(csr string) *SignCertificateRequest { + return &SignCertificateRequest{CSR: csr} +} + +// Creates a new SignCertificateResponse, containing all required fields. Optional fields may be set afterwards. +func NewSignCertificateResponse(status types.GenericStatus) *SignCertificateResponse { + return &SignCertificateResponse{Status: status} +} diff --git a/ocpp2.1/smartcharging/clear_charging_profile.go b/ocpp2.1/smartcharging/clear_charging_profile.go new file mode 100644 index 00000000..2d707892 --- /dev/null +++ b/ocpp2.1/smartcharging/clear_charging_profile.go @@ -0,0 +1,91 @@ +package smartcharging + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Clear Charging Profile (CSMS -> CS) -------------------- + +const ClearChargingProfileFeatureName = "ClearChargingProfile" + +// Status reported in ClearChargingProfileResponse. +type ClearChargingProfileStatus string + +const ( + ClearChargingProfileStatusAccepted ClearChargingProfileStatus = "Accepted" + ClearChargingProfileStatusUnknown ClearChargingProfileStatus = "Unknown" +) + +type ClearChargingProfileType struct { + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` + ChargingProfilePurpose types.ChargingProfilePurposeType `json:"chargingProfilePurpose,omitempty" validate:"omitempty,chargingProfilePurpose21"` + StackLevel *int `json:"stackLevel,omitempty" validate:"omitempty,gt=0"` +} + +func isValidClearChargingProfileStatus(fl validator.FieldLevel) bool { + status := ClearChargingProfileStatus(fl.Field().String()) + switch status { + case ClearChargingProfileStatusAccepted, ClearChargingProfileStatusUnknown: + return true + default: + return false + } +} + +// The field definition of the ClearChargingProfile request payload sent by the CSMS to the Charging Station. +type ClearChargingProfileRequest struct { + ChargingProfileID *int `json:"chargingProfileId,omitempty" validate:"omitempty"` + ChargingProfileCriteria *ClearChargingProfileType `json:"chargingProfileCriteria,omitempty" validate:"omitempty"` +} + +// This field definition of the ClearChargingProfile response payload, sent by the Charging Station to the CSMS in response to a ClearChargingProfileRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClearChargingProfileResponse struct { + Status ClearChargingProfileStatus `json:"status" validate:"required,clearChargingProfileStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// If the CSMS wishes to clear some or all of the charging profiles that were previously sent the Charging Station, +// it SHALL send a ClearChargingProfileRequest. +// The CSMS can use this message to clear (remove) either a specific charging profile (denoted by id) or a selection of +// charging profiles that match with the values of the optional connectorId, stackLevel and chargingProfilePurpose fields. +// The Charging Station SHALL respond with a ClearChargingProfileResponse payload specifying whether it was able to process the request. +type ClearChargingProfileFeature struct{} + +func (f ClearChargingProfileFeature) GetFeatureName() string { + return ClearChargingProfileFeatureName +} + +func (f ClearChargingProfileFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearChargingProfileRequest{}) +} + +func (f ClearChargingProfileFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearChargingProfileResponse{}) +} + +func (r ClearChargingProfileRequest) GetFeatureName() string { + return ClearChargingProfileFeatureName +} + +func (c ClearChargingProfileResponse) GetFeatureName() string { + return ClearChargingProfileFeatureName +} + +// Creates a new ClearChargingProfileRequest. All fields are optional and may be set afterwards. +func NewClearChargingProfileRequest() *ClearChargingProfileRequest { + return &ClearChargingProfileRequest{} +} + +// Creates a new ClearChargingProfileResponse, containing all required fields. There are no optional fields for this message. +func NewClearChargingProfileResponse(status ClearChargingProfileStatus) *ClearChargingProfileResponse { + return &ClearChargingProfileResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("clearChargingProfileStatus21", isValidClearChargingProfileStatus) +} diff --git a/ocpp2.1/smartcharging/cleared_charging_limit.go b/ocpp2.1/smartcharging/cleared_charging_limit.go new file mode 100644 index 00000000..b9ef0e45 --- /dev/null +++ b/ocpp2.1/smartcharging/cleared_charging_limit.go @@ -0,0 +1,59 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Cleared Charging Limit (CS -> CSMS) -------------------- + +const ClearedChargingLimitFeatureName = "ClearedChargingLimit" + +// The field definition of the ClearedChargingLimit request payload sent by the Charging Station to the CSMS. +type ClearedChargingLimitRequest struct { + ChargingLimitSource types.ChargingLimitSourceType `json:"chargingLimitSource" validate:"required,chargingLimitSource21"` + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` +} + +// This field definition of the ClearedChargingLimit response payload, sent by the CSMS to the Charging Station in response to a ClearedChargingLimitRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClearedChargingLimitResponse struct { +} + +// When an external control system sends a signal to release a previously imposed charging limit to a Charging Station, +// the Charging Station sends a ClearedChargingLimitRequest to notify the CSMS about this. +// The CSMS acknowledges with a ClearedChargingLimitResponse to the Charging Station. +// When the change has impact on an ongoing charging transaction and is more than: LimitChangeSignificance, +// the Charging Station needs to send a TransactionEventRequest to notify the CSMS. +type ClearedChargingLimitFeature struct{} + +func (f ClearedChargingLimitFeature) GetFeatureName() string { + return ClearedChargingLimitFeatureName +} + +func (f ClearedChargingLimitFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearedChargingLimitRequest{}) +} + +func (f ClearedChargingLimitFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearedChargingLimitResponse{}) +} + +func (r ClearedChargingLimitRequest) GetFeatureName() string { + return ClearedChargingLimitFeatureName +} + +func (c ClearedChargingLimitResponse) GetFeatureName() string { + return ClearedChargingLimitFeatureName +} + +// Creates a new ClearedChargingLimitRequest, containing all required fields. Optional fields may be set afterwards. +func NewClearedChargingLimitRequest(chargingLimitSource types.ChargingLimitSourceType) *ClearedChargingLimitRequest { + return &ClearedChargingLimitRequest{ChargingLimitSource: chargingLimitSource} +} + +// Creates a new ClearedChargingLimitResponse, which doesn't contain any required or optional fields. +func NewClearedChargingLimitResponse() *ClearedChargingLimitResponse { + return &ClearedChargingLimitResponse{} +} diff --git a/ocpp2.1/smartcharging/get_charging_profiles.go b/ocpp2.1/smartcharging/get_charging_profiles.go new file mode 100644 index 00000000..163d5836 --- /dev/null +++ b/ocpp2.1/smartcharging/get_charging_profiles.go @@ -0,0 +1,93 @@ +package smartcharging + +import ( + "reflect" + + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Get Charging Profiles (CSMS -> Charging Station) -------------------- + +const GetChargingProfilesFeatureName = "GetChargingProfiles" + +// Status reported in GetChargingProfilesResponse. +type GetChargingProfileStatus string + +const ( + GetChargingProfileStatusAccepted GetChargingProfileStatus = "Accepted" + GetChargingProfileStatusNoProfiles GetChargingProfileStatus = "NoProfiles" +) + +func isValidGetChargingProfileStatus(fl validator.FieldLevel) bool { + status := GetChargingProfileStatus(fl.Field().String()) + switch status { + case GetChargingProfileStatusAccepted, GetChargingProfileStatusNoProfiles: + return true + default: + return false + } +} + +// 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,chargingProfilePurpose21"` + 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,chargingLimitSource21"` +} + +// The field definition of the GetChargingProfiles request payload sent by the CSMS to the Charging Station. +type GetChargingProfilesRequest struct { + RequestID int `json:"requestId"` + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` + ChargingProfile ChargingProfileCriterion `json:"chargingProfile" validate:"required"` +} + +// This field definition of the GetChargingProfiles response payload, sent by the Charging Station to the CSMS in response to a GetChargingProfilesRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetChargingProfilesResponse struct { + Status GetChargingProfileStatus `json:"status" validate:"required,getChargingProfileStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS MAY ask a Charging Station to report all, or a subset of all the install Charging Profiles from the different possible sources, by sending a GetChargingProfilesRequest. +// This can be used for some automatic smart charging control system, or for debug purposes by a CSO. +// The Charging Station SHALL respond, indicating if it can report Charging Schedules by sending a GetChargingProfilesResponse message. +type GetChargingProfilesFeature struct{} + +func (f GetChargingProfilesFeature) GetFeatureName() string { + return GetChargingProfilesFeatureName +} + +func (f GetChargingProfilesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetChargingProfilesRequest{}) +} + +func (f GetChargingProfilesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetChargingProfilesResponse{}) +} + +func (r GetChargingProfilesRequest) GetFeatureName() string { + return GetChargingProfilesFeatureName +} + +func (c GetChargingProfilesResponse) GetFeatureName() string { + return GetChargingProfilesFeatureName +} + +// Creates a new GetChargingProfilesRequest, containing all required fields. Optional fields may be set afterwards. +func NewGetChargingProfilesRequest(chargingProfile ChargingProfileCriterion) *GetChargingProfilesRequest { + return &GetChargingProfilesRequest{ChargingProfile: chargingProfile} +} + +// Creates a new GetChargingProfilesResponse, containing all required fields. Optional fields may be set afterwards. +func NewGetChargingProfilesResponse(status GetChargingProfileStatus) *GetChargingProfilesResponse { + return &GetChargingProfilesResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("getChargingProfileStatus21", isValidGetChargingProfileStatus) +} diff --git a/ocpp2.1/smartcharging/get_composite_schedule.go b/ocpp2.1/smartcharging/get_composite_schedule.go new file mode 100644 index 00000000..8f104c8c --- /dev/null +++ b/ocpp2.1/smartcharging/get_composite_schedule.go @@ -0,0 +1,91 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Get Composite Schedule (CSMS -> CS) -------------------- + +const GetCompositeScheduleFeatureName = "GetCompositeSchedule" + +// Status reported in GetCompositeScheduleResponse. +type GetCompositeScheduleStatus string + +const ( + GetCompositeScheduleStatusAccepted GetCompositeScheduleStatus = "Accepted" + GetCompositeScheduleStatusRejected GetCompositeScheduleStatus = "Rejected" +) + +func isValidGetCompositeScheduleStatus(fl validator.FieldLevel) bool { + status := GetCompositeScheduleStatus(fl.Field().String()) + switch status { + case GetCompositeScheduleStatusAccepted, GetCompositeScheduleStatusRejected: + return true + default: + return false + } +} + +type CompositeSchedule struct { + StartDateTime *types.DateTime `json:"startDateTime,omitempty" validate:"omitempty"` + ChargingSchedule *types.ChargingSchedule `json:"chargingSchedule,omitempty" validate:"omitempty"` +} + +// 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,chargingRateUnit21"` + EvseID int `json:"evseId" validate:"gte=0"` +} + +// This field definition of the GetCompositeSchedule response payload, sent by the Charging System to the CSMS in response to a GetCompositeScheduleRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetCompositeScheduleResponse struct { + Status GetCompositeScheduleStatus `json:"status" validate:"required,getCompositeScheduleStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` + Schedule *CompositeSchedule `json:"schedule,omitempty" validate:"omitempty"` +} + +// The CSMS MAY request the Charging System to report the Composite Charging Schedule by sending a GetCompositeScheduleRequest. +// The Charging System SHALL calculate the Composite Charging Schedule intervals, from the moment the request payload is received: Time X, up to X + Duration, and send them in the GetCompositeScheduleResponse to the CSMS. +// The reported schedule, in the GetCompositeScheduleResponse payload, is the result of the calculation of all active schedules and possible local limits present in the Charging System. +// If the ConnectorId in the request is set to '0', the Charging System SHALL report the total expected power or current the Charging System expects to consume from the grid during the requested time period. +// If the Charging System is not able to report the requested schedule, for instance if the connectorId is unknown, it SHALL respond with a status Rejected. +type GetCompositeScheduleFeature struct{} + +func (f GetCompositeScheduleFeature) GetFeatureName() string { + return GetCompositeScheduleFeatureName +} + +func (f GetCompositeScheduleFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetCompositeScheduleRequest{}) +} + +func (f GetCompositeScheduleFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetCompositeScheduleResponse{}) +} + +func (r GetCompositeScheduleRequest) GetFeatureName() string { + return GetCompositeScheduleFeatureName +} + +func (c GetCompositeScheduleResponse) GetFeatureName() string { + return GetCompositeScheduleFeatureName +} + +// Creates a new GetCompositeScheduleRequest, containing all required fields. Optional fields may be set afterwards. +func NewGetCompositeScheduleRequest(duration int, evseId int) *GetCompositeScheduleRequest { + return &GetCompositeScheduleRequest{Duration: duration, EvseID: evseId} +} + +// Creates a new GetCompositeScheduleResponse, containing all required fields. Optional fields may be set afterwards. +func NewGetCompositeScheduleResponse(status GetCompositeScheduleStatus) *GetCompositeScheduleResponse { + return &GetCompositeScheduleResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("getCompositeScheduleStatus21", isValidGetCompositeScheduleStatus) +} diff --git a/ocpp2.1/smartcharging/notify_charging_limit.go b/ocpp2.1/smartcharging/notify_charging_limit.go new file mode 100644 index 00000000..87f0bf98 --- /dev/null +++ b/ocpp2.1/smartcharging/notify_charging_limit.go @@ -0,0 +1,70 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Notify Charging Limit (CS -> CSMS) -------------------- + +const NotifyChargingLimitFeatureName = "NotifyChargingLimit" + +// ChargingLimit contains the source of the charging limit and whether it is grid critical. +type ChargingLimit struct { + ChargingLimitSource types.ChargingLimitSourceType `json:"chargingLimitSource" validate:"required,chargingLimitSource21,max=20"` // Represents the source of the charging limit. + IsGridCritical *bool `json:"isGridCritical,omitempty" validate:"omitempty"` // Indicates whether the charging limit is critical for the grid. + IsLocalGeneration *bool `json:"isLocalGeneration,omitempty" validate:"omitempty"` // Indicates whether the charging limit is due to local generation. +} + +// The field definition of the NotifyChargingLimit request payload sent by the Charging Station to the CSMS. +type NotifyChargingLimitRequest struct { + EvseID *int `json:"evseId,omitempty" validate:"omitempty,gte=0"` + ChargingLimit ChargingLimit `json:"chargingLimit" validate:"required"` + ChargingSchedule []types.ChargingSchedule `json:"chargingSchedule,omitempty" validate:"omitempty,dive"` +} + +// This field definition of the NotifyChargingLimit response payload, sent by the CSMS to the Charging Station in response to a NotifyChargingLimitRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyChargingLimitResponse struct { +} + +// When an external control system sends a signal to release a previously imposed charging limit to a Charging Station, +// the Charging Station adjusts the charging speed of the ongoing transaction(s). +// If the charging limit changed by more than: LimitChangeSignificance, the Charging Station sends a NotifyChargingLimitRequest message to CSMS with optionally the set charging +// limit/schedule. +// +// The CSMS responds with NotifyChargingLimitResponse to the Charging Station. +// +// If the charging rate changes by more than: LimitChangeSignificance, the Charging Station sends a TransactionEventRequest message to inform the CSMS. +type NotifyChargingLimitFeature struct{} + +func (f NotifyChargingLimitFeature) GetFeatureName() string { + return NotifyChargingLimitFeatureName +} + +func (f NotifyChargingLimitFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyChargingLimitRequest{}) +} + +func (f NotifyChargingLimitFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyChargingLimitResponse{}) +} + +func (r NotifyChargingLimitRequest) GetFeatureName() string { + return NotifyChargingLimitFeatureName +} + +func (c NotifyChargingLimitResponse) GetFeatureName() string { + return NotifyChargingLimitFeatureName +} + +// Creates a new NotifyChargingLimitRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyChargingLimitRequest(chargingLimit ChargingLimit) *NotifyChargingLimitRequest { + return &NotifyChargingLimitRequest{ChargingLimit: chargingLimit} +} + +// Creates a new NotifyChargingLimitResponse, which doesn't contain any required or optional fields. +func NewNotifyChargingLimitResponse() *NotifyChargingLimitResponse { + return &NotifyChargingLimitResponse{} +} diff --git a/ocpp2.1/smartcharging/notify_ev_charging_needs.go b/ocpp2.1/smartcharging/notify_ev_charging_needs.go new file mode 100644 index 00000000..ede44e26 --- /dev/null +++ b/ocpp2.1/smartcharging/notify_ev_charging_needs.go @@ -0,0 +1,168 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Notify EV Charging Needs (CS -> CSMS) -------------------- + +const NotifyEVChargingNeedsFeatureName = "NotifyEVChargingNeeds" + +func isValidEnergyTransferMode(fl validator.FieldLevel) bool { + status := types.EnergyTransferMode(fl.Field().String()) + switch status { + case types.EnergyTransferModeAC1Phase, types.EnergyTransferModeAC2Phase, types.EnergyTransferModeAC3Phase, types.EnergyTransferModeDC: + return true + default: + return false + } +} + +// EVChargingNeedsStatus contains the status returned by the CSMS. +type EVChargingNeedsStatus string + +const ( + EVChargingNeedsStatusAccepted EVChargingNeedsStatus = "Accepted" + EVChargingNeedsStatusRejected EVChargingNeedsStatus = "Rejected" + EVChargingNeedsStatusNoChargingProfile EVChargingNeedsStatus = "NoChargingProfile" + EVChargingNeedsStatusProcessing EVChargingNeedsStatus = "Processing" +) + +func isValidEVChargingNeedsStatus(fl validator.FieldLevel) bool { + status := EVChargingNeedsStatus(fl.Field().String()) + switch status { + case EVChargingNeedsStatusAccepted, EVChargingNeedsStatusRejected, EVChargingNeedsStatusProcessing, EVChargingNeedsStatusNoChargingProfile: + return true + default: + return false + } +} + +// ACChargingParameters contains EV AC charging parameters. Used by ChargingNeeds. +type ACChargingParameters struct { + EnergyAmount int `json:"energyAmount" validate:"gte=0"` // Amount of energy requested (in Wh). This includes energy required for preconditioning. + EVMinCurrent int `json:"evMinCurrent" validate:"gte=0"` // Minimum current (amps) supported by the electric vehicle (per phase). + EVMaxCurrent int `json:"evMaxCurrent" validate:"gte=0"` // Maximum current (amps) supported by the electric vehicle (per phase). Includes cable capacity. + EVMaxVoltage int `json:"evMaxVoltage" validate:"gte=0"` // Maximum voltage supported by the electric vehicle. +} + +// DCChargingParameters contains EV DC charging parameters. Used by ChargingNeeds. +type DCChargingParameters struct { + EVMaxCurrent int `json:"evMaxCurrent" validate:"gte=0"` // Maximum current (amps) supported by the electric vehicle (per phase). Includes cable capacity. + EVMaxVoltage int `json:"evMaxVoltage" validate:"gte=0"` // Maximum voltage supported by the electric vehicle. + EnergyAmount *int `json:"energyAmount,omitempty" validate:"omitempty,gte=0"` // Amount of energy requested (in Wh). This includes energy required for preconditioning. + EVMaxPower *int `json:"evMaxPower,omitempty" validate:"omitempty,gte=0"` // Maximum power (in W) supported by the electric vehicle. Required for DC charging. + StateOfCharge *int `json:"stateOfCharge,omitempty" validate:"omitempty,gte=0,lte=100"` // Energy available in the battery (in percent of the battery capacity). + EVEnergyCapacity *int `json:"evEnergyCapacity,omitempty" validate:"omitempty,gte=0"` // Capacity of the electric vehicle battery (in Wh) + FullSoC *int `json:"fullSoC,omitempty" validate:"omitempty,gte=0,lte=100"` // Percentage of SoC at which the EV considers the battery fully charged. (possible values: 0 - 100) + BulkSoC *int `json:"bulkSoC,omitempty" validate:"omitempty,gte=0,lte=100"` // Percentage of SoC at which the EV considers a fast charging process to end. (possible values: 0 - 100) +} + +// ChargingNeeds contains the characteristics of the energy delivery required. Used by NotifyEVChargingNeedsRequest. +type ChargingNeeds struct { + RequestedEnergyTransfer types.EnergyTransferMode `json:"requestedEnergyTransfer" validate:"required,energyTransferMode21"` // Mode of energy transfer requested by the EV. + DepartureTime *types.DateTime `json:"departureTime,omitempty" validate:"omitempty"` // Estimated departure time of the EV. + ACChargingParameters *ACChargingParameters `json:"acChargingParameters,omitempty" validate:"omitempty,dive"` // AC charging parameters. + DCChargingParameters *DCChargingParameters `json:"dcChargingParameters,omitempty" validate:"omitempty,dive"` // DC charging parameters. + AvailableEnergyTransfer []types.EnergyTransferMode `json:"availableEnergyTransfer,omitempty" validate:"omitempty,energyTransferMode21"` // Energy transfer modes supported by the Charging Station. + ControlMode *ControlMode `json:"controlMode,omitempty" validate:"omitempty,controlMode"` + MobilityNeedsMode *MobilityNeedsMode `json:"mobilityNeedsMode,omitempty" validate:"omitempty,mobilityNeedsMode21"` // Mobility needs mode requested by the EV. + V2XCharginParameters *V2XChargingParameters `json:"v2xChargingParameters,omitempty" validate:"omitempty,dive"` // V2X charging parameters. + EvEnergyOffer *EvEnergyOffer `json:"evEnergyOffer,omitempty" validate:"omitempty,dive"` // EV energy offer. + DERChargingParameters *DERChargingParameters `json:"derChargingParameters,omitempty" validate:"omitempty,dive"` // DER charging parameters. +} + +type DERChargingParameters struct { + EVSupportedDERControl []string `json:"evSupportedDERControl,omitempty" validate:"omitempty"` // Supported DER control modes by the EV. + EVOverExcitedMaxDischargePower *float64 `json:"evOverExcitedMaxDischargePower,omitempty" validate:"omitempty"` + EVOverExcitedPowerFactor *float64 `json:"evOverExcitedPowerFactor,omitempty" validate:"omitempty"` + EVUnderExcitedMaxDischargePower *float64 `json:"evUnderExcitedMaxDischargePower,omitempty" validate:"omitempty"` + EVUnderExcitedPowerFactor *float64 `json:"evUnderExcitedPowerFactor,omitempty" validate:"omitempty"` + MaxApparentPower *float64 `json:"maxApparentPower,omitempty" validate:"omitempty"` // Maximum apparent power (in VA) supported by the EV. + MaxChargeApparentPower *float64 `json:"maxChargeApparentPower,omitempty" validate:"omitempty"` + MaxChargeApparentPowerL2 *float64 `json:"maxChargeApparentPower_L2,omitempty" validate:"omitempty"` + MaxChargeApparentPowerL3 *float64 `json:"maxChargeApparentPower_L3,omitempty" validate:"omitempty"` + MaxDischargeApparentPower *float64 `json:"maxDischargeApparentPower,omitempty" validate:"omitempty"` + MaxDischargeApparentPowerL2 *float64 `json:"maxDischargeApparentPower_L2,omitempty" validate:"omitempty"` + MaxDischargeApparentPowerL3 *float64 `json:"maxDischargeApparentPower_L3,omitempty" validate:"omitempty"` + MaxChargeReactivePower *float64 `json:"maxChargeReactivePower,omitempty" validate:"omitempty"` + MaxChargeReactivePowerL2 *float64 `json:"maxChargeReactivePower_L2,omitempty" validate:"omitempty"` + MaxChargeReactivePowerL3 *float64 `json:"maxChargeReactivePower_L3,omitempty" validate:"omitempty"` +} + +type V2XChargingParameters struct { +} + +type EvEnergyOffer struct { +} + +type ControlMode string + +const () + +type MobilityNeedsMode string + +const () + +// The field definition of the NotifyEVChargingNeeds request payload sent by the Charging Station to the CSMS. +type NotifyEVChargingNeedsRequest struct { + MaxScheduleTuples *int `json:"maxScheduleTuples,omitempty" validate:"omitempty,gte=0"` + EvseID int `json:"evseId" validate:"gt=0"` + Timestamp *types.DateTime `json:"timestamp,omitempty" validate:"omitempty"` + ChargingNeeds ChargingNeeds `json:"chargingNeeds" validate:"required"` +} + +// This field definition of the NotifyEVChargingNeeds response payload, sent by the CSMS to the Charging Station in response to a NotifyEVChargingNeedsRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyEVChargingNeedsResponse struct { + // Returns whether the CSMS has been able to process the message successfully. + // It does not imply that the evChargingNeeds can be met with the current charging profile. + Status EVChargingNeedsStatus `json:"status" validate:"required,evChargingNeedsStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` // Detailed status information. +} + +// When an EV sends a ChargeParameterDiscoveryReq with with charging needs parameters, +// the Charging Station sends this information in a NotifyEVChargingNeedsRequest to the CSMS. +// The CSMS replies to the Charging Station with a NotifyEVChargingNeedsResponse message. +// +// The CSMS will re-calculate a new charging schedule, trying to accomodate the EV needs, +// then asynchronously send a SetChargingProfileRequest with the new schedule to the Charging Station. +type NotifyEVChargingNeedsFeature struct{} + +func (f NotifyEVChargingNeedsFeature) GetFeatureName() string { + return NotifyEVChargingNeedsFeatureName +} + +func (f NotifyEVChargingNeedsFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyEVChargingNeedsRequest{}) +} + +func (f NotifyEVChargingNeedsFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyEVChargingNeedsResponse{}) +} + +func (r NotifyEVChargingNeedsRequest) GetFeatureName() string { + return NotifyEVChargingNeedsFeatureName +} + +func (c NotifyEVChargingNeedsResponse) GetFeatureName() string { + return NotifyEVChargingNeedsFeatureName +} + +// Creates a new NotifyEVChargingNeedsRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyEVChargingNeedsRequest(evseID int, chargingNeeds ChargingNeeds) *NotifyEVChargingNeedsRequest { + return &NotifyEVChargingNeedsRequest{EvseID: evseID, ChargingNeeds: chargingNeeds} +} + +// Creates a new NotifyEVChargingNeedsResponse, containing all required fields. Optional fields may be set afterwards. +func NewNotifyEVChargingNeedsResponse(status EVChargingNeedsStatus) *NotifyEVChargingNeedsResponse { + return &NotifyEVChargingNeedsResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("energyTransferMode21", isValidEnergyTransferMode) + _ = types.Validate.RegisterValidation("evChargingNeedsStatus21", isValidEVChargingNeedsStatus) +} diff --git a/ocpp2.1/smartcharging/notify_ev_charging_schedule.go b/ocpp2.1/smartcharging/notify_ev_charging_schedule.go new file mode 100644 index 00000000..cfee4533 --- /dev/null +++ b/ocpp2.1/smartcharging/notify_ev_charging_schedule.go @@ -0,0 +1,64 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Notify EV Charging Schedule (CS -> CSMS) -------------------- + +const NotifyEVChargingScheduleFeatureName = "NotifyEVChargingSchedule" + +// The field definition of the NotifyEVChargingSchedule request payload sent by the Charging Station to the CSMS. +type NotifyEVChargingScheduleRequest struct { + TimeBase *types.DateTime `json:"timeBase" validate:"required"` + EvseID int `json:"evseId" validate:"gt=0"` + SelectedChargingSchedule *int `json:"selectedChargingSchedule,omitempty" validate:"omitempty,gte=1"` + PowerToleranceAcceptance *bool `json:"powerToleranceAcceptance,omitempty" validate:"omitempty"` + ChargingSchedule types.ChargingSchedule `json:"chargingSchedule" validate:"required,dive"` +} + +// This field definition of the NotifyEVChargingSchedule response payload, sent by the CSMS to the Charging Station in response to a NotifyEVChargingScheduleRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type NotifyEVChargingScheduleResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` // Detailed status information. +} + +// A power renegotiation, either initiated by the EV or by the CSMS, may involve the EV providing a power profile. +// If a charging profile was provided, after receiving a PowerDeliveryResponse from the CSMS, +// the Charging Station will send a NotifyEVChargingScheduleRequest to the CSMS. +// +// The CSMS replies to the Charging Station with a NotifyEVChargingScheduleResponse. +type NotifyEVChargingScheduleFeature struct{} + +func (f NotifyEVChargingScheduleFeature) GetFeatureName() string { + return NotifyEVChargingScheduleFeatureName +} + +func (f NotifyEVChargingScheduleFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyEVChargingScheduleRequest{}) +} + +func (f NotifyEVChargingScheduleFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyEVChargingScheduleResponse{}) +} + +func (r NotifyEVChargingScheduleRequest) GetFeatureName() string { + return NotifyEVChargingScheduleFeatureName +} + +func (c NotifyEVChargingScheduleResponse) GetFeatureName() string { + return NotifyEVChargingScheduleFeatureName +} + +// Creates a new NotifyEVChargingScheduleRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyEVChargingScheduleRequest(timeBase *types.DateTime, evseID int, chargingSchedule types.ChargingSchedule) *NotifyEVChargingScheduleRequest { + return &NotifyEVChargingScheduleRequest{TimeBase: timeBase, EvseID: evseID, ChargingSchedule: chargingSchedule} +} + +// Creates a new NotifyEVChargingScheduleResponse, containing all required fields. Optional fields may be set afterwards. +func NewNotifyEVChargingScheduleResponse(status types.GenericStatus) *NotifyEVChargingScheduleResponse { + return &NotifyEVChargingScheduleResponse{Status: status} +} diff --git a/ocpp2.1/smartcharging/report_charging_profiles.go b/ocpp2.1/smartcharging/report_charging_profiles.go new file mode 100644 index 00000000..d0623dc8 --- /dev/null +++ b/ocpp2.1/smartcharging/report_charging_profiles.go @@ -0,0 +1,65 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Report Charging Profiles (CS -> CSMS) -------------------- + +const ReportChargingProfilesFeatureName = "ReportChargingProfiles" + +// The field definition of the ReportChargingProfiles request payload sent by the Charging Station to the CSMS. +type ReportChargingProfilesRequest struct { + RequestID int `json:"requestId" validate:"gte=0"` + ChargingLimitSource types.ChargingLimitSourceType `json:"chargingLimitSource" validate:"required,chargingLimitSource21"` + Tbc bool `json:"tbc,omitempty" validate:"omitempty"` + EvseID int `json:"evseId" validate:"gte=0"` + ChargingProfile []types.ChargingProfile `json:"chargingProfile" validate:"required,min=1,dive"` +} + +// This field definition of the ReportChargingProfiles response payload, sent by the CSMS to the Charging Station in +// response to a ReportChargingProfilesRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ReportChargingProfilesResponse struct { +} + +// The CSMS can ask a Charging Station to report all, or a subset of all the install Charging Profiles +// from the different possible sources. This can be used for some automatic smart charging control system, +// or for debug purposes by a CSO. This is done via the GetChargingProfiles feature. +// +// A Charging Station sends a number of ReportChargingProfilesRequest messages asynchronously to the CSMS, +// after having previously received a GetChargingProfilesRequest. The CSMS acknowledges reception of the +// reports by sending a ReportChargingProfilesResponse to the Charging Station for every received request. +type ReportChargingProfilesFeature struct{} + +func (f ReportChargingProfilesFeature) GetFeatureName() string { + return ReportChargingProfilesFeatureName +} + +func (f ReportChargingProfilesFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ReportChargingProfilesRequest{}) +} + +func (f ReportChargingProfilesFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ReportChargingProfilesResponse{}) +} + +func (r ReportChargingProfilesRequest) GetFeatureName() string { + return ReportChargingProfilesFeatureName +} + +func (c ReportChargingProfilesResponse) GetFeatureName() string { + return ReportChargingProfilesFeatureName +} + +// Creates a new ReportChargingProfilesRequest, containing all required fields. Optional fields may be set afterwards. +func NewReportChargingProfilesRequest(requestID int, chargingLimitSource types.ChargingLimitSourceType, evseID int, chargingProfile []types.ChargingProfile) *ReportChargingProfilesRequest { + return &ReportChargingProfilesRequest{RequestID: requestID, ChargingLimitSource: chargingLimitSource, EvseID: evseID, ChargingProfile: chargingProfile} +} + +// Creates a new ReportChargingProfilesResponse, which doesn't contain any required or optional fields. +func NewReportChargingProfilesResponse() *ReportChargingProfilesResponse { + return &ReportChargingProfilesResponse{} +} diff --git a/ocpp2.1/smartcharging/set_charging_profile.go b/ocpp2.1/smartcharging/set_charging_profile.go new file mode 100644 index 00000000..749d8b9c --- /dev/null +++ b/ocpp2.1/smartcharging/set_charging_profile.go @@ -0,0 +1,91 @@ +package smartcharging + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// -------------------- Set Charging Profile (CSMS -> CS) -------------------- + +const SetChargingProfileFeatureName = "SetChargingProfile" + +// Status reported in SetChargingProfileResponse, indicating whether the Charging Station processed +// the message successfully. This does not guarantee the schedule will be followed to the letter. +type ChargingProfileStatus string + +const ( + ChargingProfileStatusAccepted ChargingProfileStatus = "Accepted" + ChargingProfileStatusRejected ChargingProfileStatus = "Rejected" +) + +func isValidChargingProfileStatus(fl validator.FieldLevel) bool { + status := ChargingProfileStatus(fl.Field().String()) + switch status { + case ChargingProfileStatusAccepted, ChargingProfileStatusRejected: + return true + default: + return false + } +} + +// The field definition of the SetChargingProfile request payload sent by the CSMS to the Charging Station. +type SetChargingProfileRequest struct { + EvseID int `json:"evseId" validate:"gte=0"` + ChargingProfile *types.ChargingProfile `json:"chargingProfile" validate:"required"` +} + +// This field definition of the SetChargingProfile response payload, sent by the Charging Station to the CSMS in response to a SetChargingProfileRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetChargingProfileResponse struct { + Status ChargingProfileStatus `json:"status" validate:"required,chargingProfileStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +// The CSMS may influence the charging power or current drawn from a specific EVSE or +// the entire Charging Station, over a period of time. +// For this purpose, the CSMS calculates a ChargingSchedule to stay within certain limits, then sends a +// SetChargingProfileRequest to the Charging Station. The charging schedule limits may be imposed by any +// external system. The Charging Station responds to this request with a SetChargingProfileResponse. +// +// While charging, the EVSE will continuously adapt the maximum current/power according to the installed +// charging profiles. +type SetChargingProfileFeature struct{} + +func (f SetChargingProfileFeature) GetFeatureName() string { + return SetChargingProfileFeatureName +} + +func (f SetChargingProfileFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetChargingProfileRequest{}) +} + +func (f SetChargingProfileFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetChargingProfileResponse{}) +} + +func (r SetChargingProfileRequest) GetFeatureName() string { + return SetChargingProfileFeatureName +} + +func (c SetChargingProfileResponse) GetFeatureName() string { + return SetChargingProfileFeatureName +} + +// Creates a new SetChargingProfileRequest, containing all required fields. There are no optional fields for this message. +func NewSetChargingProfileRequest(evseID int, chargingProfile *types.ChargingProfile) *SetChargingProfileRequest { + return &SetChargingProfileRequest{ + EvseID: evseID, + ChargingProfile: chargingProfile, + } +} + +// Creates a new SetChargingProfileResponse, containing all required fields. Optional fields may be set afterwards. +func NewSetChargingProfileResponse(status ChargingProfileStatus) *SetChargingProfileResponse { + return &SetChargingProfileResponse{Status: status} +} + +func init() { + _ = types.Validate.RegisterValidation("chargingProfileStatus21", isValidChargingProfileStatus) +} diff --git a/ocpp2.1/smartcharging/smart_charging.go b/ocpp2.1/smartcharging/smart_charging.go new file mode 100644 index 00000000..5b6faaa0 --- /dev/null +++ b/ocpp2.1/smartcharging/smart_charging.go @@ -0,0 +1,47 @@ +// The Smart charging functional block contains OCPP 2.1 features that enable the CSO (or a third party) to influence the charging current/power transferred during a transaction, or set limits to the amount of current/power a Charging Station can draw from the grid. +package smartcharging + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" +) + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Smart charging profile. +type CSMSHandler interface { + // OnClearedChargingLimit is called on the CSMS whenever a ClearedChargingLimitRequest is received from a charging station. + OnClearedChargingLimit(chargingStationID string, request *ClearedChargingLimitRequest) (response *ClearedChargingLimitResponse, err error) + // OnNotifyChargingLimit is called on the CSMS whenever a NotifyChargingLimitRequest is received from a charging station. + OnNotifyChargingLimit(chargingStationID string, request *NotifyChargingLimitRequest) (response *NotifyChargingLimitResponse, err error) + // OnNotifyEVChargingNeeds is called on the CSMS whenever a NotifyEVChargingNeedsRequest is received from a charging station. + OnNotifyEVChargingNeeds(chargingStationID string, request *NotifyEVChargingNeedsRequest) (response *NotifyEVChargingNeedsResponse, err error) + // OnNotifyEVChargingSchedule is called on the CSMS whenever a NotifyEVChargingScheduleRequest is received from a charging station. + OnNotifyEVChargingSchedule(chargingStationID string, request *NotifyEVChargingScheduleRequest) (response *NotifyEVChargingScheduleResponse, err error) + // OnReportChargingProfiles is called on the CSMS whenever a ReportChargingProfilesRequest is received from a charging station. + OnReportChargingProfiles(chargingStationID string, request *ReportChargingProfilesRequest) (reponse *ReportChargingProfilesResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Smart charging profile. +type ChargingStationHandler interface { + // OnClearChargingProfile is called on a charging station whenever a ClearChargingProfileRequest is received from the CSMS. + OnClearChargingProfile(request *ClearChargingProfileRequest) (response *ClearChargingProfileResponse, err error) + // OnGetChargingProfiles is called on a charging station whenever a GetChargingProfilesRequest is received from the CSMS. + OnGetChargingProfiles(request *GetChargingProfilesRequest) (response *GetChargingProfilesResponse, err error) + // OnGetCompositeSchedule is called on a charging station whenever a GetCompositeScheduleRequest is received from the CSMS. + OnGetCompositeSchedule(request *GetCompositeScheduleRequest) (response *GetCompositeScheduleResponse, err error) + // OnSetChargingProfile is called on a charging station whenever a SetChargingProfileRequest is received from the CSMS. + OnSetChargingProfile(request *SetChargingProfileRequest) (response *SetChargingProfileResponse, err error) +} + +const ProfileName = "SmartCharging" + +var Profile = ocpp.NewProfile( + ProfileName, + ClearChargingProfileFeature{}, + ClearedChargingLimitFeature{}, + GetChargingProfilesFeature{}, + GetCompositeScheduleFeature{}, + NotifyChargingLimitFeature{}, + NotifyEVChargingNeedsFeature{}, + NotifyEVChargingScheduleFeature{}, + ReportChargingProfilesFeature{}, + SetChargingProfileFeature{}, +) diff --git a/ocpp2.1/tariffcost/change_transaction_tariff.go b/ocpp2.1/tariffcost/change_transaction_tariff.go new file mode 100644 index 00000000..3551b906 --- /dev/null +++ b/ocpp2.1/tariffcost/change_transaction_tariff.go @@ -0,0 +1,94 @@ +package tariffcost + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" + "reflect" +) + +type TariffChangeStatus string + +const ( + TariffChangeStatusAccepted TariffChangeStatus = "Accepted" + TariffChangeStatusRejected TariffChangeStatus = "Rejected" + TariffChangeStatusTooManyElements TariffChangeStatus = "TooManyElements" + TariffChangeStatusConditionNotSupported TariffChangeStatus = "ConditionNotSupported" + TariffChangeStatusTxNotFound TariffChangeStatus = "TxNotFound" + TariffChangeStatusNoCurrencyChange TariffChangeStatus = "NoCurrencyChange" +) + +func isValidTariffChangeStatus(fl validator.FieldLevel) bool { + switch TariffChangeStatus(fl.Field().String()) { + case TariffChangeStatusAccepted, + TariffChangeStatusRejected, + TariffChangeStatusTooManyElements, + TariffChangeStatusConditionNotSupported, + TariffChangeStatusTxNotFound, + TariffChangeStatusNoCurrencyChange: + return true + default: + return false + } +} + +func init() { + _ = ocppj.Validate.RegisterValidation("tariffChangeStatus", isValidTariffChangeStatus) +} + +// -------------------- Change Transaction Tariff (CSMS -> CS) -------------------- + +const ChangeTransactionTariff = "ChangeTransactionTariff" + +// The field definition of the ChangeTransactionTariff request payload sent by the CSMS to the Charging Station. +type ChangeTransactionTariffRequest struct { + TransactionId string `json:"transactionId" validate:"required,max=36"` + Tariff types.Tariff `json:"tariff" validate:"required,dive"` +} + +// This field definition of the ChangeTransactionTariff response payload, sent by the Charging Station to the CSMS in response to a ChangeTransactionTariffRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ChangeTransactionTariffResponse struct { + Status TariffSetStatus `json:"status" validate:"required,tariffChangeStatus"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +// The driver wants to know how much the running total cost is, updated at a relevant interval, while a transaction is ongoing. +// To fulfill this requirement, the CSMS sends a ChangeTransactionTariffRequest to the Charging Station to update the current total cost, every Y seconds. +// Upon receipt of the ChangeTransactionTariffRequest, the Charging Station responds with a ChangeTransactionTariffResponse, then shows the updated cost to the driver. +type ChangeTransactionTariffFeature struct{} + +func (f ChangeTransactionTariffFeature) GetFeatureName() string { + return ChangeTransactionTariff +} + +func (f ChangeTransactionTariffFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ChangeTransactionTariffRequest{}) +} + +func (f ChangeTransactionTariffFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ChangeTransactionTariffResponse{}) +} + +func (r ChangeTransactionTariffRequest) GetFeatureName() string { + return ChangeTransactionTariff +} + +func (c ChangeTransactionTariffResponse) GetFeatureName() string { + return ChangeTransactionTariff +} + +// Creates a new ChangeTransactionTariffRequest, containing all required fields. There are no optional fields for this message. +func NewChangeTransactionTariffRequest(transactionId string, tariff types.Tariff) *ChangeTransactionTariffRequest { + return &ChangeTransactionTariffRequest{ + TransactionId: transactionId, + Tariff: tariff, + } +} + +// Creates a new ChangeTransactionTariffResponse, which doesn't contain any required or optional fields. +func NewChangeTransactionTariffResponse(status TariffSetStatus) *ChangeTransactionTariffResponse { + return &ChangeTransactionTariffResponse{ + Status: status, + } +} diff --git a/ocpp2.1/tariffcost/clear_tariffs.go b/ocpp2.1/tariffcost/clear_tariffs.go new file mode 100644 index 00000000..68b16277 --- /dev/null +++ b/ocpp2.1/tariffcost/clear_tariffs.go @@ -0,0 +1,81 @@ +package tariffcost + +import ( + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" + "reflect" +) + +type TariffClearStatus string + +const ( + TariffClearStatusAccepted TariffClearStatus = "Accepted" + TariffClearStatusRejected TariffClearStatus = "Rejected" + TariffClearStatusNotSupported TariffClearStatus = "NotSupported" +) + +func isValidTariffClearStatus(fl validator.FieldLevel) bool { + status := fl.Field().String() + switch TariffClearStatus(status) { + case TariffClearStatusAccepted, TariffClearStatusRejected, TariffClearStatusNotSupported: + return true + default: + return false + } +} + +func init() { + _ = ocppj.Validate.RegisterValidation("tariffClearStatus21", isValidTariffClearStatus) +} + +// -------------------- Clear Tariffs (CSMS -> CS) -------------------- + +const ClearTariffs = "ClearTariffs" + +// The field definition of the ClearTariffsRequest request payload sent by the CSMS to the Charging Station. +type ClearTariffsRequest struct { + TariffIds []string `json:"tariffIds,omitempty" validate:"omitempty"` + EvseId int `json:"evseId,omitempty" validate:"omitempty,gte=0"` +} + +// This field definition of the ClearTariffs response payload, sent by the Charging Station to the CSMS in response to a ClearTariffsRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClearTariffsResponse struct { + ClearTariffsResult []ClearTariffsResult `json:"clearTariffsResult" validate:"required,min=1,dive"` +} + +type ClearTariffsFeature struct{} + +func (f ClearTariffsFeature) GetFeatureName() string { + return ClearTariffs +} + +func (f ClearTariffsFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClearTariffsRequest{}) +} + +func (f ClearTariffsFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClearTariffsResponse{}) +} + +func (r ClearTariffsRequest) GetFeatureName() string { + return ClearTariffs +} + +func (c ClearTariffsResponse) GetFeatureName() string { + return ClearTariffs +} + +// Creates a new NewClearTariffsRequest, containing all required fields. There are no optional fields for this message. +func NewClearTariffsRequest(tariffIds []string) *ClearTariffsRequest { + return &ClearTariffsRequest{ + TariffIds: tariffIds, + } +} + +// Creates a new NewClearTariffsResponse, which doesn't contain any required or optional fields. +func NewClearTariffsResponse(results []ClearTariffsResult) *ClearTariffsResponse { + return &ClearTariffsResponse{ + results, + } +} diff --git a/ocpp2.1/tariffcost/cost_updated.go b/ocpp2.1/tariffcost/cost_updated.go new file mode 100644 index 00000000..7ac901f8 --- /dev/null +++ b/ocpp2.1/tariffcost/cost_updated.go @@ -0,0 +1,55 @@ +package tariffcost + +import ( + "reflect" +) + +// -------------------- Cost Updated (CSMS -> CS) -------------------- + +const CostUpdatedFeatureName = "CostUpdated" + +// The field definition of the CostUpdated request payload sent by the CSMS to the Charging Station. +type CostUpdatedRequest struct { + TotalCost float64 `json:"totalCost" validate:"required"` + TransactionID string `json:"transactionId" validate:"required,max=36"` +} + +// This field definition of the CostUpdated response payload, sent by the Charging Station to the CSMS in response to a CostUpdatedRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type CostUpdatedResponse struct { +} + +// The driver wants to know how much the running total cost is, updated at a relevant interval, while a transaction is ongoing. +// To fulfill this requirement, the CSMS sends a CostUpdatedRequest to the Charging Station to update the current total cost, every Y seconds. +// Upon receipt of the CostUpdatedRequest, the Charging Station responds with a CostUpdatedResponse, then shows the updated cost to the driver. +type CostUpdatedFeature struct{} + +func (f CostUpdatedFeature) GetFeatureName() string { + return CostUpdatedFeatureName +} + +func (f CostUpdatedFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(CostUpdatedRequest{}) +} + +func (f CostUpdatedFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(CostUpdatedResponse{}) +} + +func (r CostUpdatedRequest) GetFeatureName() string { + return CostUpdatedFeatureName +} + +func (c CostUpdatedResponse) GetFeatureName() string { + return CostUpdatedFeatureName +} + +// Creates a new CostUpdatedRequest, containing all required fields. There are no optional fields for this message. +func NewCostUpdatedRequest(totalCost float64, transactionID string) *CostUpdatedRequest { + return &CostUpdatedRequest{TotalCost: totalCost, TransactionID: transactionID} +} + +// Creates a new CostUpdatedResponse, which doesn't contain any required or optional fields. +func NewCostUpdatedResponse() *CostUpdatedResponse { + return &CostUpdatedResponse{} +} diff --git a/ocpp2.1/tariffcost/get_tariffs.go b/ocpp2.1/tariffcost/get_tariffs.go new file mode 100644 index 00000000..f30f9487 --- /dev/null +++ b/ocpp2.1/tariffcost/get_tariffs.go @@ -0,0 +1,67 @@ +package tariffcost + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- Get Tariffs (CSMS -> CS) -------------------- + +const GetTariffsFeatureName = "GetTariffs" + +// The field definition of the GetTariffsRequest request payload sent by the CSMS to the Charging Station. +type GetTariffsRequest struct { + EvseId int `json:"evseId" validate:"required,gte=0"` +} + +// This field definition of the GetTariffs response payload, sent by the Charging Station to the CSMS in response to a CostUpdatedRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetTariffsResponse struct { + Status TariffGetStatus `json:"status" validate:"required,tariffGetStatus21"` + TariffAssignments TariffAssignment `json:"tariffAssignments,omitempty" validate:"omitempty,dive"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +type TariffAssignment struct { + TariffId string `json:"tariffId" validate:"required"` // The ID of the tariff. + TariffKind string `json:"tariffKind" validate:"required,tariffKind21"` + ValidFrom *types.DateTime `json:"validFrom,omitempty" validate:"omitempty"` + EvseIds []int `json:"evseIds,omitempty" validate:"omitempty"` + IdTokens []string `json:"idTokens,omitempty" validate:"omitempty"` +} + +type GetTariffsFeature struct{} + +func (f GetTariffsFeature) GetFeatureName() string { + return GetTariffsFeatureName +} + +func (f GetTariffsFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetTariffsRequest{}) +} + +func (f GetTariffsFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetTariffsResponse{}) +} + +func (r GetTariffsRequest) GetFeatureName() string { + return GetTariffsFeatureName +} + +func (c GetTariffsResponse) GetFeatureName() string { + return GetTariffsFeatureName +} + +// Creates a new GetTariffsRequest, containing all required fields. There are no optional fields for this message. +func NewGetTariffsRequest(evseId int) *GetTariffsRequest { + return &GetTariffsRequest{ + evseId, + } +} + +// Creates a new NewGetTariffsResponse, which doesn't contain any required or optional fields. +func NewGetTariffsResponse(status TariffGetStatus) *GetTariffsResponse { + return &GetTariffsResponse{ + Status: status, + } +} diff --git a/ocpp2.1/tariffcost/set_default_tariff.go b/ocpp2.1/tariffcost/set_default_tariff.go new file mode 100644 index 00000000..56c1b2d3 --- /dev/null +++ b/ocpp2.1/tariffcost/set_default_tariff.go @@ -0,0 +1,92 @@ +package tariffcost + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" + "reflect" +) + +type TariffSetStatus string + +const ( + TariffSetStatusAccepted TariffSetStatus = "Accepted" + TariffSetStatusRejected TariffSetStatus = "Rejected" + TariffSetStatusTooManyElements TariffSetStatus = "TooManyElements" + TariffSetStatusDuplicateTariffId TariffSetStatus = "DuplicateTariffId" + TariffSetStatusConditionNotSupported TariffSetStatus = "ConditionNotSupported" +) + +func init() { + _ = ocppj.Validate.RegisterValidation("tariffSetStatus", isValidTariffSetStatus) +} + +func isValidTariffSetStatus(level validator.FieldLevel) bool { + switch TariffSetStatus(level.Field().String()) { + case TariffSetStatusAccepted, + TariffSetStatusRejected, + TariffSetStatusTooManyElements, + TariffSetStatusDuplicateTariffId, + TariffSetStatusConditionNotSupported: + return true + default: + return false + } +} + +// -------------------- Set Default Tariff (CSMS -> CS) -------------------- + +const SetDefaultTariffFeatureName = "SetDefaultTariff" + +// The field definition of the SetDefaultTariff request payload sent by the CSMS to the Charging Station. +type SetDefaultTariffRequest struct { + EvseId int `json:"evseId" validate:"required,gte=0"` + Tariff types.Tariff `json:"tariff" validate:"required,dive"` +} + +// This field definition of the SetDefaultTariff response payload, sent by the Charging Station to the CSMS in response to a SetDefaultTariffRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type SetDefaultTariffResponse struct { + Status TariffSetStatus `json:"status" validate:"required,tariffSetStatus"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +// The driver wants to know how much the running total cost is, updated at a relevant interval, while a transaction is ongoing. +// To fulfill this requirement, the CSMS sends a SetDefaultTariffRequest to the Charging Station to update the current total cost, every Y seconds. +// Upon receipt of the SetDefaultTariffRequest, the Charging Station responds with a SetDefaultTariffResponse, then shows the updated cost to the driver. +type SetDefaultTariffFeature struct{} + +func (f SetDefaultTariffFeature) GetFeatureName() string { + return SetDefaultTariffFeatureName +} + +func (f SetDefaultTariffFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(SetDefaultTariffRequest{}) +} + +func (f SetDefaultTariffFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(SetDefaultTariffResponse{}) +} + +func (r SetDefaultTariffRequest) GetFeatureName() string { + return SetDefaultTariffFeatureName +} + +func (c SetDefaultTariffResponse) GetFeatureName() string { + return SetDefaultTariffFeatureName +} + +// Creates a new SetDefaultTariffRequest, containing all required fields. There are no optional fields for this message. +func NewSetDefaultTariffRequest(evseId int, tariff types.Tariff) *SetDefaultTariffRequest { + return &SetDefaultTariffRequest{ + EvseId: evseId, + Tariff: tariff, + } +} + +// Creates a new SetDefaultTariffResponse, which doesn't contain any required or optional fields. +func NewSetDefaultTariffResponse(status TariffSetStatus) *SetDefaultTariffResponse { + return &SetDefaultTariffResponse{ + Status: status, + } +} diff --git a/ocpp2.1/tariffcost/tariff_cost.go b/ocpp2.1/tariffcost/tariff_cost.go new file mode 100644 index 00000000..bcfc075c --- /dev/null +++ b/ocpp2.1/tariffcost/tariff_cost.go @@ -0,0 +1,24 @@ +// The authorization functional block contains OCPP 2.1 features that show tariff and costs to an EV driver, when supported by the charging station. +package tariffcost + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 Tariff and cost profile. +type CSMSHandler interface { +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Tariff and cost profile. +type ChargingStationHandler interface { + // OnCostUpdated is called on a charging station whenever a CostUpdatedRequest is received from the CSMS. + OnCostUpdated(request *CostUpdatedRequest) (confirmation *CostUpdatedResponse, err error) +} + +const ProfileName = "TariffCost" + +var Profile = ocpp.NewProfile( + ProfileName, + CostUpdatedFeature{}, + SetDefaultTariffFeature{}, + GetTariffsFeature{}, + ClearTariffsFeature{}, +) diff --git a/ocpp2.1/tariffcost/types.go b/ocpp2.1/tariffcost/types.go new file mode 100644 index 00000000..97888714 --- /dev/null +++ b/ocpp2.1/tariffcost/types.go @@ -0,0 +1,49 @@ +package tariffcost + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" +) + +type TariffGetStatus string + +type TariffKind string + +const ( + TariffGetStatusAccepted TariffGetStatus = "Accepted" + TariffGetStatusRejected TariffGetStatus = "Rejected" + TariffGetStatusNoTariff TariffGetStatus = "NoTariff" + + TariffKindDefaultTariff TariffKind = "DefaultTariff" + TariffKindDriverTariff TariffKind = "DriverTariff" +) + +func isValidTariffKind(fl validator.FieldLevel) bool { + switch TariffKind(fl.Field().String()) { + case TariffKindDefaultTariff, TariffKindDriverTariff: + return true + default: + return false + } +} + +func isValidTariffGetStatus(fl validator.FieldLevel) bool { + switch TariffGetStatus(fl.Field().String()) { + case TariffGetStatusAccepted, TariffGetStatusRejected, TariffGetStatusNoTariff: + return true + default: + return false + } +} + +func init() { + _ = ocppj.Validate.RegisterValidation("tariffKind21", isValidTariffKind) + _ = ocppj.Validate.RegisterValidation("tariffGetStatus21", isValidTariffGetStatus) +} + +type ClearTariffsResult struct { + TariffId string `json:"tariffId,omitempty" validate:"omitempty,max=60"` + Status TariffClearStatus `json:"status" validate:"required,tariffClearStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} diff --git a/ocpp2.1/transactions/get_transaction_status.go b/ocpp2.1/transactions/get_transaction_status.go new file mode 100644 index 00000000..d1a3f0ba --- /dev/null +++ b/ocpp2.1/transactions/get_transaction_status.go @@ -0,0 +1,57 @@ +package transactions + +import ( + "reflect" +) + +// -------------------- Clear Cache (CSMS -> CS) -------------------- + +const GetTransactionStatusFeatureName = "GetTransactionStatus" + +// The field definition of the GetTransactionStatus request payload sent by the CSMS to the Charging Station. +type GetTransactionStatusRequest struct { + TransactionID string `json:"transactionId,omitempty" validate:"omitempty,max=36"` +} + +// This field definition of the GetTransactionStatus response payload, sent by the Charging Station to the CSMS in response to a GetTransactionStatusRequest. +// 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"` + 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. +// The CSMS shall ask if the Charging Station has still messages in the queue for this transaction with the GetTransactionStatusRequest. +// It may optionally specify a transactionId, to know if a transaction is still ongoing. +// Upon receiving a GetTransactionStatusRequest, the Charging Station shall respond with a GetTransactionStatusResponse payload. +type GetTransactionStatusFeature struct{} + +func (f GetTransactionStatusFeature) GetFeatureName() string { + return GetTransactionStatusFeatureName +} + +func (f GetTransactionStatusFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetTransactionStatusRequest{}) +} + +func (f GetTransactionStatusFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetTransactionStatusResponse{}) +} + +func (r GetTransactionStatusRequest) GetFeatureName() string { + return GetTransactionStatusFeatureName +} + +func (c GetTransactionStatusResponse) GetFeatureName() string { + return GetTransactionStatusFeatureName +} + +// Creates a new GetTransactionStatusRequest, which doesn't contain any required or optional fields. +func NewGetTransactionStatusRequest() *GetTransactionStatusRequest { + return &GetTransactionStatusRequest{} +} + +// Creates a new GetTransactionStatusResponse, containing all required fields. There are no optional fields for this message. +func NewGetTransactionStatusResponse(messagesInQueue bool) *GetTransactionStatusResponse { + return &GetTransactionStatusResponse{MessagesInQueue: messagesInQueue} +} diff --git a/ocpp2.1/transactions/transaction_event.go b/ocpp2.1/transactions/transaction_event.go new file mode 100644 index 00000000..99320b90 --- /dev/null +++ b/ocpp2.1/transactions/transaction_event.go @@ -0,0 +1,99 @@ +package transactions + +import ( + "reflect" + + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" +) + +// -------------------- Transaction Event (CS -> CSMS) -------------------- + +const TransactionEventFeatureName = "TransactionEvent" + +// Contains transaction specific information. +type Transaction struct { + TransactionID string `json:"transactionId" validate:"required,max=36"` + ChargingState ChargingState `json:"chargingState,omitempty" validate:"omitempty,chargingState21"` + TimeSpentCharging *int `json:"timeSpentCharging,omitempty" validate:"omitempty"` // Contains the total time that energy flowed from EVSE to EV during the transaction (in seconds). + StoppedReason Reason `json:"stoppedReason,omitempty" validate:"omitempty,stoppedReason21"` + RemoteStartID *int `json:"remoteStartId,omitempty" validate:"omitempty"` + OperationMode types.OperationMode `json:"operationMode,omitempty" validate:"omitempty,operationMode"` + TariffId *string `json:"tariffId,omitempty" validate:"omitempty,max=60"` + TransactionLimit *TransactionLimit `json:"transactionLimit,omitempty" validate:"omitempty,dive"` +} + +// The field definition of the TransactionEvent request payload sent by the Charging Station to the CSMS. +type TransactionEventRequest struct { + EventType TransactionEvent `json:"eventType" validate:"required,transactionEven21"` + Timestamp *types.DateTime `json:"timestamp" validate:"required"` + TriggerReason TriggerReason `json:"triggerReason" validate:"required,triggerReason21"` + SequenceNo int `json:"seqNo" validate:"gte=0"` + 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. + PreconditioningStatus *PreconditioningStatus `json:"preconditioningStatus,omitempty" validate:"omitempty,preconditioningStatus"` // The preconditioning status of the EV. + EvseSleep *bool `json:"evseSleep,omitempty"` + TransactionInfo Transaction `json:"transactionInfo" validate:"required"` // Contains transaction specific information. + 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. + // todo CostDetails types.Cos +} + +// This field definition of the TransactionEventResponse payload, sent by the CSMS to the Charging Station in response to a TransactionEventRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type TransactionEventResponse struct { + TotalCost *float64 `json:"totalCost,omitempty" validate:"omitempty,gte=0"` // SHALL only be sent when charging has ended. Final total cost of this transaction, including taxes. To indicate a free transaction, the CSMS SHALL send 0.00. + ChargingPriority *int `json:"chargingPriority,omitempty" validate:"omitempty,min=-9,max=9"` // Priority from a business point of view. Default priority is 0, The range is from -9 to 9. + IDTokenInfo *types.IdTokenInfo `json:"idTokenInfo,omitempty" validate:"omitempty"` // Is required when the transactionEventRequest contained an idToken. + UpdatedPersonalMessage *types.MessageContent `json:"updatedPersonalMessage,omitempty" validate:"omitempty,dive"` // This can contain updated personal message that can be shown to the EV Driver. This can be used to provide updated tariff information. + UpdatedPersonalMessageExtra []types.MessageContent `json:"updatedPersonalMessageExtra,omitempty" validate:"omitempty,max=4,dive"` // This can contain updated personal message that can be shown to the EV Driver. This can be used to provide updated tariff information. + TransactionLimit *TransactionLimit `json:"transactionLimit,omitempty" validate:"omitempty,dive"` // Contains the transaction limit for this transaction. This can be used to inform the Charging Station about the maximum cost, time, energy or SoC for this transaction. +} + +// Gives the CSMS information that will later be used to bill a transaction. +// For this purpose, status changes and additional transaction-related information is sent, such as +// retrying and sequence number messages. +// +// A Charging Station notifies the CSMS using a TransactionEventRequest. The CSMS then responds with a +// TransactionEventResponse. +type TransactionEventFeature struct{} + +func (f TransactionEventFeature) GetFeatureName() string { + return TransactionEventFeatureName +} + +func (f TransactionEventFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(TransactionEventRequest{}) +} + +func (f TransactionEventFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(TransactionEventResponse{}) +} + +func (r TransactionEventRequest) GetFeatureName() string { + return TransactionEventFeatureName +} + +func (c TransactionEventResponse) GetFeatureName() string { + return TransactionEventFeatureName +} + +// Creates a new TransactionEventRequest, containing all required fields. Optional fields may be set afterwards. +func NewTransactionEventRequest(t TransactionEvent, timestamp *types.DateTime, reason TriggerReason, seqNo int, info Transaction) *TransactionEventRequest { + return &TransactionEventRequest{EventType: t, Timestamp: timestamp, TriggerReason: reason, SequenceNo: seqNo, TransactionInfo: info} +} + +// Creates a new TransactionEventResponse, containing all required fields. Optional fields may be set afterwards. +func NewTransactionEventResponse() *TransactionEventResponse { + return &TransactionEventResponse{} +} + +func init() { + _ = types.Validate.RegisterValidation("transactionEvent21", isValidTransactionEvent) + _ = types.Validate.RegisterValidation("triggerReason21", isValidTriggerReason) + _ = types.Validate.RegisterValidation("chargingState21", isValidChargingState) + _ = types.Validate.RegisterValidation("stoppedReason21", isValidReason) + _ = types.Validate.RegisterValidation("preconditioningStatus", isValidPreconditioningStatus) +} diff --git a/ocpp2.1/transactions/transactions.go b/ocpp2.1/transactions/transactions.go new file mode 100644 index 00000000..59dfc758 --- /dev/null +++ b/ocpp2.1/transactions/transactions.go @@ -0,0 +1,24 @@ +// The transactions functional block contains OCPP 2.0 features related to OCPP transactions. +package transactions + +import "github.com/lorenzodonini/ocpp-go/ocpp" + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.0 Transactions profile. +type CSMSHandler interface { + // OnTransactionEvent is called on the CSMS whenever a TransactionEventRequest is received from a charging station. + OnTransactionEvent(chargingStationID string, request *TransactionEventRequest) (response *TransactionEventResponse, err error) +} + +// Needs to be implemented by Charging stations for handling messages part of the OCPP 2.0 Transactions profile. +type ChargingStationHandler interface { + // OnGetTransactionStatusResponse is called on a charging station whenever a OnGetTransactionStatusRequest is received from the CSMS. + OnGetTransactionStatus(request *GetTransactionStatusRequest) (response *GetTransactionStatusResponse, err error) +} + +const ProfileName = "Transactions" + +var Profile = ocpp.NewProfile( + ProfileName, + GetTransactionStatusFeature{}, + TransactionEventFeature{}, +) diff --git a/ocpp2.1/transactions/types.go b/ocpp2.1/transactions/types.go new file mode 100644 index 00000000..c44fef96 --- /dev/null +++ b/ocpp2.1/transactions/types.go @@ -0,0 +1,154 @@ +package transactions + +import "gopkg.in/go-playground/validator.v9" + +// The type of a transaction event. +type TransactionEvent string + +// Reason that triggered a TransactionEventRequest. +type TriggerReason string + +// The state of the charging process. +type ChargingState string + +// Reason for stopping a transaction. +type Reason string + +const ( + TransactionEventStarted TransactionEvent = "Started" // First event of a transaction. + TransactionEventUpdated TransactionEvent = "Updated" // Transaction event in between 'Started' and 'Ended'. + TransactionEventEnded TransactionEvent = "Ended" // Last event of a transaction + + TriggerReasonAuthorized TriggerReason = "Authorized" // Charging is authorized, by any means. + TriggerReasonCablePluggedIn TriggerReason = "CablePluggedIn" // Cable is plugged in and EVDetected. + TriggerReasonChargingRateChanged TriggerReason = "ChargingRateChanged" // Rate of charging changed by more than LimitChangeSignificance. + TriggerReasonChargingStateChanged TriggerReason = "ChargingStateChanged" // Charging state changed. + TriggerReasonDeAuthorized TriggerReason = "Deauthorized" // The transaction was stopped because of the authorization status in the response to a transactionEventRequest. + TriggerReasonEnergyLimitReached TriggerReason = "EnergyLimitReached" // Maximum energy of charging reached. For example: in a pre-paid charging solution + TriggerReasonEVCommunicationLost TriggerReason = "EVCommunicationLost" // Communication with EV lost, for example: cable disconnected. + TriggerReasonEVConnectTimeout TriggerReason = "EVConnectTimeout" // EV not connected before the connection is timed out. + TriggerReasonMeterValueClock TriggerReason = "MeterValueClock" // Needed to send a clock aligned meter value. + TriggerReasonMeterValuePeriodic TriggerReason = "MeterValuePeriodic" // Needed to send a periodic meter value. + TriggerReasonTimeLimitReached TriggerReason = "TimeLimitReached" // Maximum time of charging reached. For example: in a pre-paid charging solution + TriggerReasonTrigger TriggerReason = "Trigger" // Requested by the CSMS via a TriggerMessageRequest. + TriggerReasonUnlockCommand TriggerReason = "UnlockCommand" // CSMS sent an Unlock Connector command. + TriggerReasonStopAuthorized TriggerReason = "StopAuthorized" // An EV Driver has been authorized to stop charging. + TriggerReasonEVDeparted TriggerReason = "EVDeparted" // EV departed. For example: When a departing EV triggers a parking bay detector. + TriggerReasonEVDetected TriggerReason = "EVDetected" // EV detected. For example: When an arriving EV triggers a parking bay detector. + TriggerReasonRemoteStop TriggerReason = "RemoteStop" // A RequestStopTransactionRequest has been sent. + TriggerReasonRemoteStart TriggerReason = "RemoteStart" // A RequestStartTransactionRequest has been sent. + TriggerReasonAbnormalCondition TriggerReason = "AbnormalCondition" // An Abnormal Error or Fault Condition has occurred. + TriggerReasonSignedDataReceived TriggerReason = "SignedDataReceived" // Signed data is received from the energy meter. + TriggerReasonResetCommand TriggerReason = "ResetCommand" // CSMS sent a Reset Charging Station command. + + ChargingStateCharging ChargingState = "Charging" // The contactor of the Connector is closed and energy is flowing to between EVSE and EV. + ChargingStateEVConnected ChargingState = "EVConnected" // There is a connection between EV and EVSE (wired or wireless). + ChargingStateSuspendedEV ChargingState = "SuspendedEV" // When the EV is connected to the EVSE and the EVSE is offering energy but the EV is not taking any energy. + ChargingStateSuspendedEVSE ChargingState = "SuspendedEVSE" // When the EV is connected to the EVSE but the EVSE is not offering energy to the EV (e.g. due to smart charging, power constraints, authorization status). + ChargingStateIdle ChargingState = "Idle" // There is no connection between EV and EVSE. + + ReasonDeAuthorized Reason = "DeAuthorized" // The transaction was stopped because of the authorization status in the response to a transactionEventRequest. + ReasonEmergencyStop Reason = "EmergencyStop" // Emergency stop button was used. + ReasonEnergyLimitReached Reason = "EnergyLimitReached" // EV charging session reached a locally enforced maximum energy transfer limit. + ReasonEVDisconnected Reason = "EVDisconnected" // Disconnecting of cable, vehicle moved away from inductive charge unit. + ReasonGroundFault Reason = "GroundFault" // A GroundFault has occurred. + ReasonImmediateReset Reason = "ImmediateReset" // A Reset(Immediate) command was received. + ReasonLocal Reason = "Local" // Stopped locally on request of the EV Driver at the Charging Station. This is a regular termination of a transaction. + ReasonLocalOutOfCredit Reason = "LocalOutOfCredit" // A local credit limit enforced through the Charging Station has been exceeded. + ReasonMasterPass Reason = "MasterPass" // The transaction was stopped using a token with a MasterPassGroupId. + ReasonOther Reason = "Other" // Any other reason. + ReasonOvercurrentFault Reason = "OvercurrentFault" // A larger than intended electric current has occurred. + ReasonPowerLoss Reason = "PowerLoss" // Complete loss of power. + ReasonPowerQuality Reason = "PowerQuality" // Quality of power too low, e.g. voltage too low/high, phase imbalance, etc. + ReasonReboot Reason = "Reboot" // A locally initiated reset/reboot occurred. + ReasonRemote Reason = "Remote" // Stopped remotely on request of the CSMS. This is a regular termination of a transaction. + ReasonSOCLimitReached Reason = "SOCLimitReached" // Electric vehicle has reported reaching a locally enforced maximum battery State of Charge (SOC). + ReasonStoppedByEV Reason = "StoppedByEV" // The transaction was stopped by the EV. + ReasonTimeLimitReached Reason = "TimeLimitReached" // EV charging session reached a locally enforced time limit. + ReasonTimeout Reason = "Timeout" // EV not connected within timeout. + ReasonCostLimitReached Reason = "CostLimitReached" // Maximum cost has been reached, as defined by transactionLimit.maxCost. + ReasonLimitSet Reason = "LimitSet" // Limit of cost/time/energy/SoC for transaction has set or changed + ReasonOperationModeChanged Reason = "OperationModeChanged" // V2X operation mode has changed (at start of a new charging schedule period). + ReasonRunningCost Reason = "RunningCost" // Trigger used when TransactionEvent is sent (only) to report a running cost update. + ReasonSoCLimitReached Reason = "SoCLimitReached" // State of charge limit has been reached, as defined by transactionLimit.maxSoC + ReasonTariffChanged Reason = "TariffChanged" // Tariff for transaction has changed. + ReasonTariffNotAccepted Reason = "TariffNotAccepted" // Trigger to notify that EV Driver has not accepted the tariff for transaction. idToken becomes deauthorized. + ReasonTxResumed Reason = "TxResumed" // Transaction has resumed after reset or power outage. +) + +func isValidTransactionEvent(fl validator.FieldLevel) bool { + status := TransactionEvent(fl.Field().String()) + switch status { + case TransactionEventStarted, TransactionEventUpdated, TransactionEventEnded: + return true + default: + return false + } +} + +func isValidTriggerReason(fl validator.FieldLevel) bool { + status := TriggerReason(fl.Field().String()) + switch status { + case TriggerReasonAuthorized, TriggerReasonCablePluggedIn, TriggerReasonChargingRateChanged, + TriggerReasonChargingStateChanged, TriggerReasonDeAuthorized, TriggerReasonEnergyLimitReached, + TriggerReasonEVCommunicationLost, TriggerReasonEVConnectTimeout, TriggerReasonMeterValueClock, + TriggerReasonMeterValuePeriodic, TriggerReasonTimeLimitReached, TriggerReasonTrigger, + TriggerReasonUnlockCommand, TriggerReasonStopAuthorized, TriggerReasonEVDeparted, + TriggerReasonEVDetected, TriggerReasonRemoteStop, TriggerReasonRemoteStart, + TriggerReasonAbnormalCondition, TriggerReasonSignedDataReceived, TriggerReasonResetCommand: + return true + default: + return false + } +} + +func isValidChargingState(fl validator.FieldLevel) bool { + status := ChargingState(fl.Field().String()) + switch status { + case ChargingStateCharging, ChargingStateEVConnected, ChargingStateSuspendedEV, ChargingStateSuspendedEVSE, ChargingStateIdle: + return true + default: + return false + } +} + +func isValidReason(fl validator.FieldLevel) bool { + status := Reason(fl.Field().String()) + switch status { + case ReasonDeAuthorized, ReasonEmergencyStop, ReasonEnergyLimitReached, ReasonEVDisconnected, + ReasonGroundFault, ReasonImmediateReset, ReasonLocal, ReasonLocalOutOfCredit, ReasonMasterPass, + ReasonOther, ReasonOvercurrentFault, ReasonPowerLoss, ReasonPowerQuality, ReasonReboot, ReasonRemote, + ReasonSOCLimitReached, ReasonStoppedByEV, ReasonTimeLimitReached, ReasonTimeout, + ReasonCostLimitReached, ReasonLimitSet, ReasonOperationModeChanged, + ReasonRunningCost, ReasonSoCLimitReached, ReasonTariffChanged, ReasonTariffNotAccepted, ReasonTxResumed: + return true + default: + return false + } +} + +type PreconditioningStatus string + +const ( + PreconditioningStatusNotReady PreconditioningStatus = "NotReady" + PreconditioningStatusReady PreconditioningStatus = "Ready" + PreconditioningStatusUnknown PreconditioningStatus = "Unknown" + PreconditioningStatusPreconditioning PreconditioningStatus = "Preconditioning" +) + +func isValidPreconditioningStatus(fl validator.FieldLevel) bool { + status := PreconditioningStatus(fl.Field().String()) + switch status { + case PreconditioningStatusNotReady, PreconditioningStatusReady, PreconditioningStatusUnknown, PreconditioningStatusPreconditioning: + return true + default: + return false + } +} + +type TransactionLimit struct { + MaxCost *float64 `json:"maxCost,omitempty" validate:"omitempty"` + MaxEnergy *float64 `json:"maxEnergy,omitempty" validate:"omitempty"` + MaxTime *int `json:"maxTime,omitempty" validate:"omitempty"` + MaxSoC *int `json:"maxSoC,omitempty" validate:"omitempty,gte=0,lte=100"` // Percentage of battery charge +} diff --git a/ocpp2.1/types/authorization.go b/ocpp2.1/types/authorization.go new file mode 100644 index 00000000..88edd30c --- /dev/null +++ b/ocpp2.1/types/authorization.go @@ -0,0 +1,105 @@ +package types + +import "gopkg.in/go-playground/validator.v9" + +type AuthorizationStatus string + +const ( + AuthorizationStatusAccepted AuthorizationStatus = "Accepted" + AuthorizationStatusBlocked AuthorizationStatus = "Blocked" + AuthorizationStatusExpired AuthorizationStatus = "Expired" + AuthorizationStatusInvalid AuthorizationStatus = "Invalid" + AuthorizationStatusConcurrentTx AuthorizationStatus = "ConcurrentTx" + AuthorizationStatusNoCredit AuthorizationStatus = "NoCredit" + AuthorizationStatusNotAllowedTypeEVSE AuthorizationStatus = "NotAllowedTypeEVSE" + AuthorizationStatusNotAtThisLocation AuthorizationStatus = "NotAtThisLocation" + AuthorizationStatusNotAtThisTime AuthorizationStatus = "NotAtThisTime" + AuthorizationStatusUnknown AuthorizationStatus = "Unknown" +) + +func isValidAuthorizationStatus(fl validator.FieldLevel) bool { + status := AuthorizationStatus(fl.Field().String()) + switch status { + case AuthorizationStatusAccepted, AuthorizationStatusBlocked, AuthorizationStatusExpired, AuthorizationStatusInvalid, AuthorizationStatusConcurrentTx, AuthorizationStatusNoCredit, AuthorizationStatusNotAllowedTypeEVSE, AuthorizationStatusNotAtThisLocation, AuthorizationStatusNotAtThisTime, AuthorizationStatusUnknown: + return true + default: + return false + } +} + +// ID Token +type IdTokenType string + +const ( + IdTokenTypeCentral IdTokenType = "Central" + IdTokenTypeEMAID IdTokenType = "eMAID" + IdTokenTypeISO14443 IdTokenType = "ISO14443" + IdTokenTypeISO15693 IdTokenType = "ISO15693" + IdTokenTypeKeyCode IdTokenType = "KeyCode" + IdTokenTypeLocal IdTokenType = "Local" + IdTokenTypeMacAddress IdTokenType = "MacAddress" + IdTokenTypeNoAuthorization IdTokenType = "NoAuthorization" +) + +func isValidIdTokenType(fl validator.FieldLevel) bool { + tokenType := IdTokenType(fl.Field().String()) + switch tokenType { + case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeISO15693, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeMacAddress, IdTokenTypeNoAuthorization: + return true + default: + return false + } +} + +func isValidIdToken(sl validator.StructLevel) { + idToken := sl.Current().Interface().(IdToken) + // validate required idToken value except `NoAuthorization` type + switch idToken.Type { + case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeISO15693, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeMacAddress: + if idToken.IdToken == "" { + sl.ReportError(idToken.IdToken, "IdToken", "IdToken", "required", "") + } + } +} + +type AdditionalInfo struct { + AdditionalIdToken string `json:"additionalIdToken" validate:"required,max=36"` + Type string `json:"type" validate:"required,max=50"` +} + +type IdToken struct { + IdToken string `json:"idToken" validate:"max=255"` + Type IdTokenType `json:"type" validate:"required,idTokenType,max=20"` + AdditionalInfo []AdditionalInfo `json:"additionalInfo,omitempty" validate:"omitempty,dive"` +} + +type GroupIdToken struct { + IdToken string `json:"idToken" validate:"max=36"` + Type IdTokenType `json:"type" validate:"required,idTokenType"` +} + +func isValidGroupIdToken(sl validator.StructLevel) { + groupIdToken := sl.Current().Interface().(GroupIdToken) + // validate required idToken value except `NoAuthorization` type + switch groupIdToken.Type { + case IdTokenTypeCentral, IdTokenTypeEMAID, IdTokenTypeISO14443, IdTokenTypeISO15693, IdTokenTypeKeyCode, IdTokenTypeLocal, IdTokenTypeMacAddress: + if groupIdToken.IdToken == "" { + sl.ReportError(groupIdToken.IdToken, "IdToken", "IdToken", "required", "") + } + } +} + +type IdTokenInfo struct { + Status AuthorizationStatus `json:"status" validate:"required,authorizationStatus21"` + 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"` + Language2 string `json:"language2,omitempty" validate:"max=8"` + GroupIdToken *GroupIdToken `json:"groupIdToken,omitempty"` + PersonalMessage *MessageContent `json:"personalMessage,omitempty"` +} + +// NewIdTokenInfo creates an IdTokenInfo. Optional parameters may be set afterwards on the initialized struct. +func NewIdTokenInfo(status AuthorizationStatus) *IdTokenInfo { + return &IdTokenInfo{Status: status} +} diff --git a/ocpp2.1/types/certificates.go b/ocpp2.1/types/certificates.go new file mode 100644 index 00000000..cb34d9b5 --- /dev/null +++ b/ocpp2.1/types/certificates.go @@ -0,0 +1,130 @@ +package types + +import "gopkg.in/go-playground/validator.v9" + +// Hash Algorithms +type HashAlgorithmType string + +const ( + SHA256 HashAlgorithmType = "SHA256" + SHA384 HashAlgorithmType = "SHA384" + SHA512 HashAlgorithmType = "SHA512" +) + +func isValidHashAlgorithmType(fl validator.FieldLevel) bool { + algorithm := HashAlgorithmType(fl.Field().String()) + switch algorithm { + case SHA256, SHA384, SHA512: + return true + default: + return false + } +} + +// OCSPRequestDataType +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=40"` + ResponderURL string `json:"responderURL,omitempty" validate:"max=512"` +} + +// CertificateHashDataType +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=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 +type Certificate15118EVStatus string + +const ( + Certificate15188EVStatusAccepted Certificate15118EVStatus = "Accepted" + Certificate15118EVStatusFailed Certificate15118EVStatus = "Failed" +) + +func isValidCertificate15118EVStatus(fl validator.FieldLevel) bool { + status := Certificate15118EVStatus(fl.Field().String()) + switch status { + case Certificate15188EVStatusAccepted, Certificate15118EVStatusFailed: + return true + default: + return false + } +} + +// Indicates the type of the signed certificate that is returned. +// When omitted the certificate is used for both the 15118 connection (if implemented) and the Charging Station to CSMS connection. +// This field is required when a typeOfCertificate was included in the SignCertificateRequest that requested this certificate to be signed AND both the 15118 connection and the Charging Station connection are implemented. +type CertificateSigningUse string + +const ( + ChargingStationCert CertificateSigningUse = "ChargingStationCertificate" + V2GCertificate CertificateSigningUse = "V2GCertificate" +) + +func isValidCertificateSigningUse(fl validator.FieldLevel) bool { + status := CertificateSigningUse(fl.Field().String()) + switch status { + case ChargingStationCert, V2GCertificate: + return true + default: + return false + } +} + +// Indicates the type of the requested certificate. +// It is used in GetInstalledCertificateIdsRequest and InstallCertificateRequest messages. +type CertificateUse string + +const ( + V2GRootCertificate CertificateUse = "V2GRootCertificate" + MORootCertificate CertificateUse = "MORootCertificate" + CSOSubCA1 CertificateUse = "CSOSubCA1" + CSOSubCA2 CertificateUse = "CSOSubCA2" + CSMSRootCertificate CertificateUse = "CSMSRootCertificate" + V2GCertificateChain CertificateUse = "V2GCertificateChain" + ManufacturerRootCertificate CertificateUse = "ManufacturerRootCertificate" + OEMRootCertificate CertificateUse = "OEMRootCertificate" +) + +func isValidCertificateUse(fl validator.FieldLevel) bool { + use := CertificateUse(fl.Field().String()) + switch use { + case V2GRootCertificate, MORootCertificate, CSOSubCA1, + CSOSubCA2, CSMSRootCertificate, V2GCertificateChain, ManufacturerRootCertificate, OEMRootCertificate: + return true + default: + return false + } +} + +// Enumeration of the cryptographic method used to create the digital signature. +// The list is expected to grow in future OCPP releases to allow other signature methods used by Smart Meters. +type SignatureMethod string + +const ( + SignatureECDSAP256SHA256 SignatureMethod = "ECDSAP256SHA256" // The encoded data is hashed with the SHA-256 hash function, and the hash value is then signed with the ECDSA algorithm using the NIST P-256 elliptic curve. + SignatureECDSAP384SHA384 SignatureMethod = "ECDSAP384SHA384" // The encoded data is hashed with the SHA-384 hash function, and the hash value is then signed with the ECDSA algorithm using the NIST P-384 elliptic curve. + SignatureECDSA192SHA256 SignatureMethod = "ECDSA192SHA256" // The encoded data is hashed with the SHA-256 hash function, and the hash value is then signed with the ECDSA algorithm using a 192-bit elliptic curve. +) + +func isValidSignatureMethod(fl validator.FieldLevel) bool { + signature := SignatureMethod(fl.Field().String()) + switch signature { + case SignatureECDSA192SHA256, SignatureECDSAP256SHA256, SignatureECDSAP384SHA384: + return true + default: + return false + } +} diff --git a/ocpp2.1/types/cost.go b/ocpp2.1/types/cost.go new file mode 100644 index 00000000..1a09b3e4 --- /dev/null +++ b/ocpp2.1/types/cost.go @@ -0,0 +1,76 @@ +package types + +import ( + "gopkg.in/go-playground/validator.v9" +) + +// Sales Tariff + +// The kind of cost referred to in a CostType. +type CostKind string + +const ( + CostKindCarbonDioxideEmission CostKind = "CarbonDioxideEmission" // Carbon Dioxide emissions, in grams per kWh. + CostKindRelativePricePercentage CostKind = "RelativePricePercentage" // Price per kWh, as percentage relative to the maximum price stated in any of all tariffs indicated to the EV. + CostKindRenewableGenerationPercentage CostKind = "RenewableGenerationPercentage" // Percentage of renewable generation within total generation. +) + +func isValidCostKind(fl validator.FieldLevel) bool { + purposeType := CostKind(fl.Field().String()) + switch purposeType { + case CostKindCarbonDioxideEmission, CostKindRelativePricePercentage, CostKindRenewableGenerationPercentage: + return true + default: + return false + } +} + +// Defines the time interval the SalesTariffEntry is valid for, based upon relative times. +type RelativeTimeInterval struct { + Start int `json:"start"` // Start of the interval, in seconds from NOW. + Duration *int `json:"duration,omitempty" validate:"omitempty,gte=0"` // Duration of the interval, in seconds. +} + +// Cost details. +type CostType struct { + CostKind CostKind `json:"costKind" validate:"required,costKind21"` // The kind of cost referred to in the message element amount. + Amount int `json:"amount" validate:"gte=0"` // The estimated or actual cost per kWh. + AmountMultiplier *int `json:"amountMultiplier,omitempty" validate:"omitempty,min=-3,max=3"` // The exponent to base 10 (dec). +} + +// Contains price information and/or alternative costs. +type ConsumptionCost struct { + StartValue float64 `json:"startValue"` // The lowest level of consumption that defines the starting point of this consumption block + Cost []CostType `json:"cost" validate:"required,max=3,dive"` // Contains the cost details. +} + +// NewConsumptionCost instantiates a new ConsumptionCost struct. No additional parameters need to be set. +func NewConsumptionCost(startValue float64, cost []CostType) ConsumptionCost { + return ConsumptionCost{ + StartValue: startValue, + Cost: cost, + } +} + +// Element describing all relevant details for one time interval of the SalesTariff. +type SalesTariffEntry struct { + EPriceLevel *int `json:"ePriceLevel,omitempty" validate:"omitempty,gte=0"` // The price level of this SalesTariffEntry (referring to NumEPriceLevels). Small values for the EPriceLevel represent a cheaper TariffEntry. + RelativeTimeInterval RelativeTimeInterval `json:"relativeTimeInterval" validate:"required"` // The time interval the SalesTariffEntry is valid for, based upon relative times. + ConsumptionCost []ConsumptionCost `json:"consumptionCost,omitempty" validate:"omitempty,max=3,dive"` // Additional means for further relative price information and/or alternative costs. +} + +// Sales tariff associated with this charging schedule. +type SalesTariff struct { + ID int `json:"id"` // Identifier used to identify one sales tariff. + SalesTariffDescription string `json:"salesTariffDescription,omitempty" validate:"omitempty,max=32"` // A human readable title/short description of the sales tariff e.g. for HMI display purposes. + NumEPriceLevels *int `json:"numEPriceLevels,omitempty" validate:"omitempty"` // Defines the overall number of distinct price levels used across all provided SalesTariff elements. + SalesTariffEntry []SalesTariffEntry `json:"salesTariffEntry" validate:"required,min=1,max=1024,dive"` // Encapsulates elements describing all relevant details for one time interval of the SalesTariff. +} + +// NewSalesTariff instantiates a new SalesTariff struct. Only required fields are passed as parameters. +func NewSalesTariff(id int, salesTariffEntries []SalesTariffEntry) *SalesTariff { + return &SalesTariff{ + ID: id, + SalesTariffEntry: salesTariffEntries, + } +} diff --git a/ocpp2.1/types/datetime.go b/ocpp2.1/types/datetime.go new file mode 100644 index 00000000..c451b0a5 --- /dev/null +++ b/ocpp2.1/types/datetime.go @@ -0,0 +1,81 @@ +package types + +import ( + "encoding/json" + "errors" + "time" + + "github.com/relvacode/iso8601" +) + +// DateTimeFormat to be used when serializing all OCPP messages. +// +// The default dateTime format is RFC3339. +// Change this if another format is desired. +var DateTimeFormat = time.RFC3339 + +// DateTime wraps a time.Time struct, allowing for improved dateTime JSON compatibility. +type DateTime struct { + time.Time +} + +// Creates a new DateTime struct, embedding a time.Time struct. +func NewDateTime(time time.Time) *DateTime { + return &DateTime{Time: time} +} + +// Creates a new DateTime struct, containing a time.Now() value. +func Now() *DateTime { + return &DateTime{Time: time.Now()} +} + +func null(b []byte) bool { + if len(b) != 4 { + return false + } + if b[0] != 'n' && b[1] != 'u' && b[2] != 'l' && b[3] != 'l' { + return false + } + return true +} + +func (dt *DateTime) UnmarshalJSON(input []byte) error { + // Do not parse null timestamps + if null(input) { + return nil + } + // Assert that timestamp is a string + if len(input) > 0 && input[0] == '"' && input[len(input)-1] == '"' { + input = input[1 : len(input)-1] + } else { + return errors.New("timestamp not enclosed in double quotes") + } + // Parse ISO8601 + var err error + dt.Time, err = iso8601.Parse(input) + return err +} + +func (dt *DateTime) MarshalJSON() ([]byte, error) { + if DateTimeFormat == "" { + return json.Marshal(dt.Time) + } + timeStr := dt.FormatTimestamp() + return json.Marshal(timeStr) +} + +// Formats the UTC timestamp using the DateTimeFormat setting. +// This function is used during JSON marshaling as well. +func (dt *DateTime) FormatTimestamp() string { + return dt.UTC().Format(DateTimeFormat) +} + +func FormatTimestamp(t time.Time) string { + return t.UTC().Format(DateTimeFormat) +} + +// DateTime Validation + +func DateTimeIsNull(dateTime *DateTime) bool { + return dateTime != nil && dateTime.IsZero() +} diff --git a/ocpp2.1/types/measurements.go b/ocpp2.1/types/measurements.go new file mode 100644 index 00000000..ad83c255 --- /dev/null +++ b/ocpp2.1/types/measurements.go @@ -0,0 +1,182 @@ +package types + +import "gopkg.in/go-playground/validator.v9" + +// Meter Value + +type ReadingContext string +type Measurand string +type Phase string +type Location string + +const ( + ReadingContextInterruptionBegin ReadingContext = "Interruption.Begin" + ReadingContextInterruptionEnd ReadingContext = "Interruption.End" + ReadingContextOther ReadingContext = "Other" + ReadingContextSampleClock ReadingContext = "Sample.Clock" + ReadingContextSamplePeriodic ReadingContext = "Sample.Periodic" + ReadingContextTransactionBegin ReadingContext = "Transaction.Begin" + ReadingContextTransactionEnd ReadingContext = "Transaction.End" + ReadingContextTrigger ReadingContext = "Trigger" + + MeasurandCurrentExport Measurand = "Current.Export" + MeasurandCurrentImport Measurand = "Current.Import" + MeasurandCurrentOffered Measurand = "Current.Offered" + MeasurandCurrentExportOffered Measurand = "Current.Export.Offered" + MeasurandCurrentExportMinimum Measurand = "Current.Export.Minimum" + MeasurandCurrentImportOffered Measurand = "Current.Import.Offered" + MeasurandCurrentImportMinimum Measurand = "Current.Import.Minimum" + + MeasurandEnergyActiveExportRegister Measurand = "Energy.Active.Export.Register" + MeasurandEnergyActiveImportRegister Measurand = "Energy.Active.Import.Register" + MeasurandEnergyReactiveExportRegister Measurand = "Energy.Reactive.Export.Register" + MeasurandEnergyReactiveImportRegister Measurand = "Energy.Reactive.Import.Register" + MeasurandEnergyActiveExportInterval Measurand = "Energy.Active.Export.Interval" + MeasurandEnergyActiveSetpointInterval Measurand = "Energy.Active.Setpoint.Interval" + MeasurandEnergyActiveImportInterval Measurand = "Energy.Active.Import.Interval" + MeasurandEnergyActiveImportCableLoss Measurand = "Energy.Active.Import.CableLoss" + MeasurandEnergyActiveImportLocalGenerationRegister Measurand = "Energy.Active.Import.LocalGeneration.Register" + MeasurandEnergyActiveNet Measurand = "Energy.Active.Net" + MeasurandEnergyReactiveExportInterval Measurand = "Energy.Reactive.Export.Interval" + MeasurandEnergyReactiveImportInterval Measurand = "Energy.Reactive.Import.Interval" + MeasurandEnergyReactiveNet Measurand = "Energy.Reactive.Net" + MeasurandEnergyApparentNet Measurand = "Energy.Apparent.Net" + MeasurandEnergyApparentImport Measurand = "Energy.Apparent.Import" + MeasurandEnergyApparentExport Measurand = "Energy.Apparent.Export" + MeasurandEnergyRequestMinimum Measurand = "EnergyRequest.Minimum" + MeasurandEnergyRequestTarget Measurand = "EnergyRequest.Target" + MeasurandEnergyRequestMaximum Measurand = "EnergyRequest.Maximum" + MeasurandEnergyRequestMinimumV2X Measurand = "EnergyRequest.Minimum.V2X" + MeasurandEnergyRequestMaximumV2X Measurand = "EnergyRequest.Maximum.V2X" + MeasurandEnergyRequestBulk Measurand = "EnergyRequest.Bulk" + + MeasurandFrequency Measurand = "Frequency" + + MeasurandPowerActiveExport Measurand = "Power.Active.Export" + MeasurandPowerActiveImport Measurand = "Power.Active.Import" + MeasurandPowerFactor Measurand = "Power.Factor" + MeasurandPowerOffered Measurand = "Power.Offered" + MeasurandPowerReactiveExport Measurand = "Power.Reactive.Export" + MeasurandPowerReactiveImport Measurand = "Power.Reactive.Import" + MeasurandPowerActiveSetpoint Measurand = "Power.Active.Setpoint" + MeasurandPowerActiveResidual Measurand = "Power.Active.Residual" + MeasurandPowerExportMinimum Measurand = "Power.Export.Minimum" + MeasurandPowerExportOffered Measurand = "Power.Export.Offered" + MeasurandPowerImportOffered Measurand = "Power.Import.Offered" + MeasurandPowerImportMinimum Measurand = "Power.Import.Minimum" + + MeasurandSoC Measurand = "SoC" + MeasurandDisplayPresentSoC = Measurand("Display.PresentSOC") + MeasurandDisplayMinimumSoC = Measurand("Display.MinimumSOC") + MeasurandDisplayTargetSoC = Measurand("Display.TargetSOC") + MeasurandDisplayMaximumSoC = Measurand("Display.MaximumSOC") + MeasurandDisplayRemainingTimeToMinimumSoC = Measurand("Display.RemainingTimeToMinimumSOC") + MeasurandDisplayRemainingTimeToTargetSoC = Measurand("Display.RemainingTimeToTargetSOC") + MeasurandDisplayRemainingTimeToMaximumSoC = Measurand("Display.RemainingTimeToMaximumSOC") + MeasurandDisplayChargingComplete = Measurand("Display.ChargingComplete") + MeasurandDisplayBatteryEnergyCapacity = Measurand("Display.BatteryEnergyCapacity") + MeasurandDisplayInletHot = Measurand("Display.InletHot") + + MeasurandTemperature Measurand = "Temperature" + + MeasurandVoltage Measurand = "Voltage" + MeasurandVoltageMinimum Measurand = "Voltage.Minimum" + MeasurandVoltageMaximum Measurand = "Voltage.Maximum" + + PhaseL1 Phase = "L1" + PhaseL2 Phase = "L2" + PhaseL3 Phase = "L3" + PhaseN Phase = "N" + PhaseL1N Phase = "L1-N" + PhaseL2N Phase = "L2-N" + PhaseL3N Phase = "L3-N" + PhaseL1L2 Phase = "L1-L2" + PhaseL2L3 Phase = "L2-L3" + PhaseL3L1 Phase = "L3-L1" + LocationBody Location = "Body" + LocationCable Location = "Cable" + LocationEV Location = "EV" + LocationInlet Location = "Inlet" + LocationOutlet Location = "Outlet" + LocationUpstream Location = "Upstream" +) + +func isValidReadingContext(fl validator.FieldLevel) bool { + readingContext := ReadingContext(fl.Field().String()) + switch readingContext { + case ReadingContextInterruptionBegin, ReadingContextInterruptionEnd, + ReadingContextOther, ReadingContextSampleClock, ReadingContextSamplePeriodic, + ReadingContextTransactionBegin, ReadingContextTransactionEnd, ReadingContextTrigger: + return true + default: + return false + } +} + +func isValidMeasurand(fl validator.FieldLevel) bool { + measurand := Measurand(fl.Field().String()) + switch measurand { + case MeasurandSoC, MeasurandCurrentExport, MeasurandCurrentImport, MeasurandCurrentOffered, MeasurandEnergyActiveExportInterval, + MeasurandEnergyActiveExportRegister, MeasurandEnergyReactiveExportInterval, MeasurandEnergyReactiveExportRegister, MeasurandEnergyReactiveImportRegister, + MeasurandEnergyReactiveImportInterval, MeasurandEnergyActiveImportInterval, MeasurandEnergyActiveImportRegister, MeasurandFrequency, MeasurandPowerActiveExport, + MeasurandPowerActiveImport, MeasurandPowerReactiveImport, MeasurandPowerReactiveExport, MeasurandPowerOffered, MeasurandPowerFactor, MeasurandVoltage, + MeasurandTemperature, MeasurandEnergyActiveNet, MeasurandEnergyApparentNet, MeasurandEnergyReactiveNet, MeasurandEnergyApparentImport, + MeasurandEnergyApparentExport, MeasurandEnergyActiveSetpointInterval, MeasurandEnergyActiveImportCableLoss, MeasurandEnergyActiveImportLocalGenerationRegister, + MeasurandEnergyRequestMinimum, MeasurandEnergyRequestTarget, MeasurandEnergyRequestMaximum, MeasurandEnergyRequestMinimumV2X, MeasurandPowerActiveSetpoint, + MeasurandPowerActiveResidual, MeasurandEnergyRequestBulk, MeasurandDisplayPresentSoC, MeasurandDisplayMinimumSoC, MeasurandDisplayTargetSoC, + MeasurandPowerExportMinimum, MeasurandDisplayMaximumSoC, MeasurandEnergyRequestMaximumV2X, MeasurandVoltageMinimum, MeasurandVoltageMaximum, + MeasurandCurrentExportOffered, MeasurandPowerExportOffered, MeasurandDisplayRemainingTimeToMinimumSoC, MeasurandDisplayChargingComplete, MeasurandCurrentExportMinimum, + MeasurandPowerImportOffered, MeasurandDisplayRemainingTimeToTargetSoC, MeasurandDisplayBatteryEnergyCapacity, MeasurandCurrentImportOffered, + MeasurandPowerImportMinimum, MeasurandDisplayRemainingTimeToMaximumSoC, MeasurandDisplayInletHot, MeasurandCurrentImportMinimum: + return true + default: + return false + } +} + +func isValidPhase(fl validator.FieldLevel) bool { + phase := Phase(fl.Field().String()) + switch phase { + case PhaseL1, PhaseL2, PhaseL3, PhaseN, PhaseL1N, PhaseL2N, PhaseL3N, PhaseL1L2, PhaseL2L3, PhaseL3L1: + return true + default: + return false + } +} + +func isValidLocation(fl validator.FieldLevel) bool { + location := Location(fl.Field().String()) + switch location { + case LocationBody, LocationCable, LocationEV, LocationInlet, LocationOutlet, LocationUpstream: + return true + default: + return false + } +} + +type UnitOfMeasure struct { + Unit string `json:"unit,omitempty" validate:"omitempty,max=20"` + Multiplier *int `json:"multiplier,omitempty" validate:"omitempty,gte=0"` +} + +type SignedMeterValue struct { + SignedMeterData string `json:"signedMeterData" validate:"required,max=32768"` // Base64 encoded, contains the signed data which might contain more then just the meter value. It can contain information like timestamps, reference to a customer etc. + SigningMethod string `json:"signingMethod,omitempty" validate:"omitempty,max=50"` // Method used to create the digital signature. + EncodingMethod string `json:"encodingMethod" validate:"required,max=50"` // Method used to encode the meter values before applying the digital signature algorithm. + PublicKey string `json:"publicKey,omitempty" validate:"omitempty,max=2500"` // Base64 encoded, sending depends on configuration variable PublicKeyWithSignedMeterValue. +} + +type SampledValue struct { + Value float64 `json:"value"` // Indicates the measured value. This value is required. + Context ReadingContext `json:"context,omitempty" validate:"omitempty,readingContext21"` // Type of detail value: start, end or sample. Default = "Sample.Periodic" + Measurand Measurand `json:"measurand,omitempty" validate:"omitempty,measurand21"` // Type of measurement. Default = "Energy.Active.Import.Register" + Phase Phase `json:"phase,omitempty" validate:"omitempty,phase21"` // 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,location21"` // 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 { + Timestamp DateTime `json:"timestamp" validate:"required"` + SampledValue []SampledValue `json:"sampledValue" validate:"required,min=1,dive"` +} diff --git a/ocpp2.1/types/smart_charging.go b/ocpp2.1/types/smart_charging.go new file mode 100644 index 00000000..cfa6f504 --- /dev/null +++ b/ocpp2.1/types/smart_charging.go @@ -0,0 +1,193 @@ +package types + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "gopkg.in/go-playground/validator.v9" +) + +// Charging Profiles +type ChargingProfilePurposeType string +type ChargingProfileKindType string +type RecurrencyKindType string +type ChargingRateUnitType string +type ChargingLimitSourceType string + +type OperationMode string + +const ( + ChargingProfilePurposeChargingStationExternalConstraints ChargingProfilePurposeType = "ChargingStationExternalConstraints" + ChargingProfilePurposeChargingStationMaxProfile ChargingProfilePurposeType = "ChargingStationMaxProfile" + ChargingProfilePurposeTxDefaultProfile ChargingProfilePurposeType = "TxDefaultProfile" + ChargingProfilePurposeTxProfile ChargingProfilePurposeType = "TxProfile" + + ChargingProfileKindAbsolute ChargingProfileKindType = "Absolute" + ChargingProfileKindRecurring ChargingProfileKindType = "Recurring" + ChargingProfileKindRelative ChargingProfileKindType = "Relative" + + RecurrencyKindDaily RecurrencyKindType = "Daily" + RecurrencyKindWeekly RecurrencyKindType = "Weekly" + + ChargingRateUnitWatts ChargingRateUnitType = "W" + ChargingRateUnitAmperes ChargingRateUnitType = "A" + + ChargingLimitSourceEMS ChargingLimitSourceType = "EMS" + ChargingLimitSourceOther ChargingLimitSourceType = "Other" + ChargingLimitSourceSO ChargingLimitSourceType = "SO" + ChargingLimitSourceCSO ChargingLimitSourceType = "CSO" + + OperationModeIdle OperationMode = "Idle" + OperationModeChargingOnly OperationMode = "ChargingOnly" + OperationModeCentralSetpoint OperationMode = "CentralSetpoint" + OperationModeExternalSetpoint OperationMode = "ExternalSetpoint" + OperationModeExternalLimits OperationMode = "ExternalLimits" + OperationModeCentralFrequency OperationMode = "CentralFrequency" + OperationModeLocalFrequency OperationMode = "LocalFrequency" + OperationModeLocalLoadBalancing OperationMode = "LocalLoadBalancing" +) + +func isValidOperationMode(fl validator.FieldLevel) bool { + operationMode := OperationMode(fl.Field().String()) + switch operationMode { + case OperationModeIdle, OperationModeChargingOnly, OperationModeCentralSetpoint, OperationModeExternalSetpoint, + OperationModeExternalLimits, OperationModeCentralFrequency, OperationModeLocalFrequency, + OperationModeLocalLoadBalancing: + return true + default: + return false + } +} + +func isValidChargingProfilePurpose(fl validator.FieldLevel) bool { + purposeType := ChargingProfilePurposeType(fl.Field().String()) + switch purposeType { + case ChargingProfilePurposeChargingStationExternalConstraints, ChargingProfilePurposeChargingStationMaxProfile, + ChargingProfilePurposeTxDefaultProfile, ChargingProfilePurposeTxProfile: + return true + default: + return false + } +} + +func isValidChargingProfileKind(fl validator.FieldLevel) bool { + purposeType := ChargingProfileKindType(fl.Field().String()) + switch purposeType { + case ChargingProfileKindAbsolute, ChargingProfileKindRecurring, ChargingProfileKindRelative: + return true + default: + return false + } +} + +func isValidRecurrencyKind(fl validator.FieldLevel) bool { + purposeType := RecurrencyKindType(fl.Field().String()) + switch purposeType { + case RecurrencyKindDaily, RecurrencyKindWeekly: + return true + default: + return false + } +} + +func isValidChargingRateUnit(fl validator.FieldLevel) bool { + purposeType := ChargingRateUnitType(fl.Field().String()) + switch purposeType { + case ChargingRateUnitWatts, ChargingRateUnitAmperes: + return true + default: + return false + } +} + +func isValidChargingLimitSource(fl validator.FieldLevel) bool { + chargingLimitSource := ChargingLimitSourceType(fl.Field().String()) + switch chargingLimitSource { + case ChargingLimitSourceEMS, ChargingLimitSourceOther, ChargingLimitSourceSO, ChargingLimitSourceCSO: + return true + default: + return false + } +} + +type ChargingSchedulePeriod struct { + StartPeriod int `json:"startPeriod" validate:"gte=0"` + NumberPhases *int `json:"numberPhases,omitempty" validate:"omitempty,gte=0,lte=3"` + Limit float64 `json:"limit" validate:"gte=0"` + LimitL2 *float64 `json:"limit_L2,omitempty" validate:"omitempty,gte=0"` + LimitL3 *float64 `json:"limit_L3,omitempty" validate:"omitempty,gte=0"` + PhaseToUse *int `json:"phaseToUse,omitempty" validate:"omitempty,gte=0,lte=3"` + DischargeLimit *float64 `json:"dischargeLimit,omitempty" validate:"omitempty,lte=0"` + DischargeLimitL2 *float64 `json:"dischargeLimit_L2,omitempty" validate:"omitempty,lte=0"` + DischargeLimitL3 *float64 `json:"dischargeLimit_L3,omitempty" validate:"omitempty,lte=0"` + SetPoint *float64 `json:"setpoint,omitempty" validate:"omitempty"` + SetPointL2 *float64 `json:"setpoint_L2,omitempty" validate:"omitempty"` + SetPointL3 *float64 `json:"setpoint_L3,omitempty" validate:"omitempty"` + SetpointReactive *float64 `json:"setpointReactive,omitempty" validate:"omitempty"` + SetpointReactiveL2 *float64 `json:"setpointReactive_L2,omitempty" validate:"omitempty"` + SetpointReactiveL3 *float64 `json:"setpointReactive_L3,omitempty" validate:"omitempty"` + OperationMode OperationMode `json:"operationMode,omitempty" validate:"omitempty,operationMode21"` + V2xFreqWattCurve []V2xFreqWattCurve `json:"v2xFreqWattCurve,omitempty" validate:"omitempty,max=20,dive"` + V2xSignalWattPoint []V2XSignalWattPoint `json:"v2xSignalWattPoint,omitempty" validate:"omitempty,max=20,dive"` +} + +type V2xFreqWattCurve struct { + Frequency float64 `json:"frequency" validate:"gte=0"` + Power float64 `json:"power" validate:"gte=0"` // Power in Watts, positive for export, negative for import. +} + +type V2XSignalWattPoint struct { + Signal int `json:"signal" validate:"required"` + Power float64 `json:"power" validate:"required"` +} + +func NewChargingSchedulePeriod(startPeriod int, limit float64) ChargingSchedulePeriod { + return ChargingSchedulePeriod{StartPeriod: startPeriod, Limit: limit} +} + +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,chargingRateUnit21"` + 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. + PowerTolerance *float64 `json:"powerTolerance,omitempty" validate:"omitempty"` + SignatureId *int `json:"signatureId,omitempty" validate:"omitempty,gte=0"` + DigestValue *string `json:"digestValue,omitempty" validate:"omitempty,max=88"` + UseLocalTime bool `json:"useLocalTime,omitempty" validate:"omitempty"` + RandomizedDelay *int `json:"randomizedDelay,omitempty" validate:"omitempty,gte=0"` +} + +func NewChargingSchedule(id int, chargingRateUnit ChargingRateUnitType, schedulePeriod ...ChargingSchedulePeriod) *ChargingSchedule { + return &ChargingSchedule{ID: id, ChargingRateUnit: chargingRateUnit, ChargingSchedulePeriod: schedulePeriod} +} + +type ChargingProfile struct { + ID int `json:"id" validate:"gte=0"` + StackLevel int `json:"stackLevel" validate:"gte=0"` + ChargingProfilePurpose ChargingProfilePurposeType `json:"chargingProfilePurpose" validate:"required,chargingProfilePurpose21"` + ChargingProfileKind ChargingProfileKindType `json:"chargingProfileKind" validate:"required,chargingProfileKind21"` + RecurrencyKind RecurrencyKindType `json:"recurrencyKind,omitempty" validate:"omitempty,recurrencyKind21"` + ValidFrom *DateTime `json:"validFrom,omitempty"` + ValidTo *DateTime `json:"validTo,omitempty"` + TransactionID string `json:"transactionId,omitempty" validate:"omitempty,max=36"` + MaxOfflineDuration *int `json:"maxOfflineDuration,omitempty" validate:"omitempty"` + InvalidAfterOfflineDuration bool `json:"invalidAfterOfflineDuration,omitempty" validate:"omitempty"` + DynUpdateInterval *int `json:"dynUpdateInterval,omitempty" validate:"omitempty"` + DynUpdateTime *types.DateTime `json:"dynUpdateTime,omitempty" validate:"omitempty"` + PriceScheduleSignature *string `json:"priceScheduleSignature,omitempty" validate:"omitempty,max=256"` + ChargingSchedule []ChargingSchedule `json:"chargingSchedule" validate:"required,min=1,max=3,dive"` +} + +func NewChargingProfile(id int, stackLevel int, chargingProfilePurpose ChargingProfilePurposeType, chargingProfileKind ChargingProfileKindType, schedule []ChargingSchedule) *ChargingProfile { + return &ChargingProfile{ID: id, StackLevel: stackLevel, ChargingProfilePurpose: chargingProfilePurpose, ChargingProfileKind: chargingProfileKind, ChargingSchedule: schedule} +} + +type EnergyTransferMode string + +const ( + EnergyTransferModeDC EnergyTransferMode = "DC" // DC charging. + EnergyTransferModeAC1Phase EnergyTransferMode = "AC_single_phase" // AC single phase charging according to IEC 62196. + EnergyTransferModeAC2Phase EnergyTransferMode = "AC_two_phase" // AC two phase charging according to IEC 62196. + EnergyTransferModeAC3Phase EnergyTransferMode = "AC_three_phase" // AC three phase charging according to IEC 62196. +) diff --git a/ocpp2.1/types/tariff.go b/ocpp2.1/types/tariff.go new file mode 100644 index 00000000..c24a2431 --- /dev/null +++ b/ocpp2.1/types/tariff.go @@ -0,0 +1,155 @@ +package types + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "gopkg.in/go-playground/validator.v9" +) + +type Tariff struct { + TariffId string `json:"tariffId" validate:"required,max=60"` // Identifier used to identify one tariff. + Currency string `json:"currency" validate:"required,max=3"` + ValidFrom *types.DateTime `json:"validFrom,omitempty" validate:"omitempty"` + Description []MessageContent `json:"description,omitempty" validate:"omitempty,max=10,dive"` + Energy *TariffEnergy `json:"energy,omitempty" validate:"omitempty,dive"` + ChargingTime *TariffTime `json:"chargingTime,omitempty" validate:"omitempty,dive"` + IdleTime *TariffTime `json:"idleTime,omitempty" validate:"omitempty,dive"` + FixedFee *TariffFixed `json:"fixedFee,omitempty" validate:"omitempty,dive"` + MinCost *Price `json:"minCost,omitempty" validate:"omitempty,dive"` + MaxCost *Price `json:"maxCost,omitempty" validate:"omitempty,dive"` + ReservationTime *TariffTime `json:"reservationTime,omitempty" validate:"omitempty,dive"` + ReservationFixed *TariffFixed `json:"reservationFixed,omitempty" validate:"omitempty,dive"` +} + +type Price struct { + ExclTax *float64 `json:"exclTax,omitempty" validate:"omitempty"` + InclTax *float64 `json:"inclTax,omitempty" validate:"omitempty"` + TaxRates []TaxRate `json:"taxRates,omitempty" validate:"omitempty,max=5,dive"` +} + +type TaxRate struct { + Type string `json:"type" validate:"required,max=20"` // Type of tax rate, e.g., VAT. + Tax float64 `json:"tax" validate:"required"` + Stack *int `json:"stack,omitempty" validate:"omitempty,gte=0"` +} + +type TaxRule struct { + TaxRuleId int `json:"taxRuleId" validate:"required,gte=0"` + TaxRuleName *string `json:"taxRuleName,omitempty" validate:"omitempty,max=100"` + TaxIncludedInPrice bool `json:"taxIncludedInPrice,omitempty" validate:"omitempty"` + AppliesToEnergyFee bool `json:"appliesToEnergyFee" validate:"required"` + AppliesToParkingFee bool `json:"appliesToParkingFee" validate:"required"` + AppliesToOverstayFee bool `json:"appliesToOverstayFee" validate:"required"` + AppliesToMinimumMaximumCost bool `json:"appliesToMinimumMaximumCost" validate:"required"` + TaxRate RationalNumber `json:"taxRate" validate:"required,dive"` // Tax rate as a rational number. +} + +type RationalNumber struct { + Exponent int `json:"exponent" validate:"required"` + Value int `json:"value" validate:"required"` +} + +type TariffTime struct { + Prices []TariffTimePrice `json:"prices" validate:"required,min=1,max=5,dive"` + TaxRates []TaxRate `json:"taxRates,omitempty" validate:"omitempty,max=5,dive"` +} + +type TariffTimePrice struct { + PriceMinute float64 `json:"priceMinute" validate:"required"` // Price per minute. + Conditions *TariffConditions `json:"conditions,omitempty" validate:"omitempty,dive"` +} + +type TariffFixed struct { + Prices []TariffFixedPrice `json:"prices" validate:"required,min=1,dive"` // Prices for fixed fees. + TaxRates []TaxRate `json:"taxRates,omitempty" validate:"omitempty,max=5,dive"` +} + +type TariffFixedPrice struct { + PriceFixed float64 `json:"priceFixed" validate:"required"` // Fixed price. + Conditions *TariffFixedConditions `json:"conditions,omitempty" validate:"omitempty,dive"` +} + +type TariffFixedConditions struct { + StartTimeOfDay *string `json:"startTimeOfDay,omitempty" validate:"omitempty"` + EndTimeOfDay *string `json:"endTimeOfDay,omitempty" validate:"omitempty"` + DayOfWeek []DayOfWeek `json:"dayOfWeek,omitempty" validate:"omitempty,dayOfWeek"` + ValidFromDate string `json:"validFromDate,omitempty" validate:"omitempty"` + ValidToDate string `json:"validToDate,omitempty" validate:"omitempty"` + EvseKind *EvseKind `json:"evseKind,omitempty" validate:"omitempty,evseKind"` + PaymentBrand *string `json:"paymentBrand,omitempty" validate:"omitempty,max=20"` + PaymentRecognition *string `json:"paymentRecognition,omitempty" validate:"omitempty,max=20"` +} + +type TariffEnergy struct { + TaxRates []TaxRate `json:"taxRates,omitempty" validate:"omitempty,max=5,dive"` + Prices []TariffEnergyPrice `json:"prices" validate:"required,min=1,dive"` // Prices for energy in kWh. +} + +type TariffEnergyPrice struct { + PriceKwh float64 `json:"priceKWh" validate:"required"` // Price per kWh. + Conditions *TariffConditions `json:"conditions,omitempty" validate:"omitempty,dive"` +} + +type EvseKind string + +const ( + EvseKindAC EvseKind = "AC" // Alternating Current + EvseKindDC EvseKind = "DC" // Direct Current +) + +func isValidEvseKind(fl validator.FieldLevel) bool { + switch EvseKind(fl.Field().String()) { + case EvseKindAC, EvseKindDC: + return true + default: + return false + } +} + +type DayOfWeek string + +const ( + DayOfWeekMonday DayOfWeek = "Monday" + DayOfWeekTuesday DayOfWeek = "Tuesday" + DayOfWeekWednesday DayOfWeek = "Wednesday" + DayOfWeekThursday DayOfWeek = "Thursday" + DayOfWeekFriday DayOfWeek = "Friday" + DayOfWeekSaturday DayOfWeek = "Saturday" + DayOfWeekSunday DayOfWeek = "Sunday" +) + +func isValidDayOfWeek(fl validator.FieldLevel) bool { + switch DayOfWeek(fl.Field().String()) { + case DayOfWeekMonday, DayOfWeekTuesday, DayOfWeekWednesday, + DayOfWeekThursday, DayOfWeekFriday, DayOfWeekSaturday, DayOfWeekSunday: + return true + default: + return false + } +} + +func init() { + _ = ocppj.Validate.RegisterValidation("evseKind", isValidEvseKind) + _ = ocppj.Validate.RegisterValidation("dayOfWeek", isValidDayOfWeek) +} + +type TariffConditions struct { + StartTimeOfDay *string `json:"startTimeOfDay,omitempty" validate:"omitempty"` + EndTimeOfDay *string `json:"endTimeOfDay,omitempty" validate:"omitempty"` + DayOfWeek []DayOfWeek `json:"dayOfWeek,omitempty" validate:"omitempty,dayOfWeek"` + ValidFromDate string `json:"validFromDate,omitempty" validate:"omitempty"` + ValidToDate string `json:"validToDate,omitempty" validate:"omitempty"` + EvseKind *EvseKind `json:"evseKind,omitempty" validate:"omitempty,evseKind"` + MinEnergy *float64 `json:"minEnergy,omitempty" validate:"omitempty"` + MaxEnergy *float64 `json:"maxEnergy,omitempty" validate:"omitempty"` + MinCurrent *float64 `json:"minCurrent,omitempty" validate:"omitempty"` + MaxCurrent *float64 `json:"maxCurrent,omitempty" validate:"omitempty"` + MinPower *float64 `json:"minPower,omitempty" validate:"omitempty"` + MaxPower *float64 `json:"maxPower,omitempty" validate:"omitempty"` + MinTime *int `json:"minTime,omitempty" validate:"omitempty"` // Minimum time in seconds. + MaxTime *int `json:"maxTime,omitempty" validate:"omitempty"` // Maximum time in seconds. + MinChargingTime *int `json:"minChargingTime,omitempty" validate:"omitempty"` + MaxChargingTime *int `json:"maxChargingTime,omitempty" validate:"omitempty"` // Maximum charging time in seconds. + MinIdleTime *int `json:"minIdleTime,omitempty" validate:"omitempty"` // Minimum idle time in seconds. + MaxIdleTime *int `json:"maxIdleTime,omitempty" validate:"omitempty"` // Maximum idle time in seconds. +} diff --git a/ocpp2.1/types/transaction.go b/ocpp2.1/types/transaction.go new file mode 100644 index 00000000..2024ac25 --- /dev/null +++ b/ocpp2.1/types/transaction.go @@ -0,0 +1,21 @@ +package types + +import "gopkg.in/go-playground/validator.v9" + +// Remote Start/Stop +type RemoteStartStopStatus string + +const ( + RemoteStartStopStatusAccepted RemoteStartStopStatus = "Accepted" + RemoteStartStopStatusRejected RemoteStartStopStatus = "Rejected" +) + +func isValidRemoteStartStopStatus(fl validator.FieldLevel) bool { + status := RemoteStartStopStatus(fl.Field().String()) + switch status { + case RemoteStartStopStatusAccepted, RemoteStartStopStatusRejected: + return true + default: + return false + } +} diff --git a/ocpp2.1/types/types.go b/ocpp2.1/types/types.go new file mode 100644 index 00000000..36f6742e --- /dev/null +++ b/ocpp2.1/types/types.go @@ -0,0 +1,199 @@ +// Contains common and shared data types between OCPP 2.1 messages. +package types + +import ( + "gopkg.in/go-playground/validator.v9" + + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +const ( + V2Subprotocol = "ocpp2.1" +) + +type PropertyViolation struct { + error + Property string +} + +func (e *PropertyViolation) Error() string { + return "" +} + +// Generic Device Model Status +type GenericDeviceModelStatus string + +const ( + GenericDeviceModelStatusAccepted GenericDeviceModelStatus = "Accepted" + GenericDeviceModelStatusRejected GenericDeviceModelStatus = "Rejected" + GenericDeviceModelStatusNotSupported GenericDeviceModelStatus = "NotSupported" + GenericDeviceModelStatusEmptyResultSet GenericDeviceModelStatus = "EmptyResultSet" // If the combination of received criteria result in an empty result set. +) + +func isValidGenericDeviceModelStatus(fl validator.FieldLevel) bool { + status := GenericDeviceModelStatus(fl.Field().String()) + switch status { + case GenericDeviceModelStatusAccepted, GenericDeviceModelStatusRejected, GenericDeviceModelStatusNotSupported, GenericDeviceModelStatusEmptyResultSet: + return true + default: + return false + } +} + +// Generic Status +type GenericStatus string + +const ( + GenericStatusAccepted GenericStatus = "Accepted" + GenericStatusRejected GenericStatus = "Rejected" +) + +func isValidGenericStatus(fl validator.FieldLevel) bool { + status := GenericStatus(fl.Field().String()) + switch status { + case GenericStatusAccepted, GenericStatusRejected: + return true + default: + return false + } +} + +// ID Token Info +type MessageFormatType string + +const ( + MessageFormatASCII MessageFormatType = "ASCII" + MessageFormatHTML MessageFormatType = "HTML" + MessageFormatURI MessageFormatType = "URI" + MessageFormatUTF8 MessageFormatType = "UTF8" +) + +func isValidMessageFormatType(fl validator.FieldLevel) bool { + algorithm := MessageFormatType(fl.Field().String()) + switch algorithm { + case MessageFormatASCII, MessageFormatHTML, MessageFormatURI, MessageFormatUTF8: + return true + default: + return false + } +} + +type MessageContent struct { + Format MessageFormatType `json:"format" validate:"required,messageFormat21"` + Language string `json:"language,omitempty" validate:"max=8"` + Content string `json:"content" validate:"required,max=1024"` +} + +// StatusInfo is an element providing more information about the message status. +type StatusInfo struct { + ReasonCode string `json:"reasonCode" validate:"required,max=20"` // A predefined code for the reason why the status is returned in this response. The string is case- insensitive. + AdditionalInfo string `json:"additionalInfo,omitempty" validate:"omitempty,max=1024"` // Additional text to provide detailed information. +} + +// NewStatusInfo creates a StatusInfo struct. +// If no additional info need to be set, an empty string may be passed. +func NewStatusInfo(reasonCode string, additionalInfo string) *StatusInfo { + return &StatusInfo{ReasonCode: reasonCode, AdditionalInfo: additionalInfo} +} + +// EVSE represents the Electric Vehicle Supply Equipment, formerly referred to as connector(s). +type EVSE struct { + ID int `json:"id" validate:"gte=0"` // The EVSE Identifier. When 0, the ID references the Charging Station as a whole. + ConnectorID *int `json:"connectorId,omitempty" validate:"omitempty,gte=0"` // An id to designate a specific connector (on an EVSE) by connector index number. +} + +// Component represents a physical or logical component. +type Component struct { + Name string `json:"name" validate:"required,max=50"` // Name of the component. Name should be taken from the list of standardized component names whenever possible. Case Insensitive. strongly advised to use Camel Case. + Instance string `json:"instance,omitempty" validate:"omitempty,max=50"` // Name of instance in case the component exists as multiple instances. Case Insensitive. strongly advised to use Camel Case. + EVSE *EVSE `json:"evse,omitempty" validate:"omitempty"` // Specifies the EVSE when component is located at EVSE level, also specifies the connector when component is located at Connector level. +} + +// Variable is a reference key to a component-variable. +type Variable struct { + Name string `json:"name" validate:"required,max=50"` // Name of the variable. Name should be taken from the list of standardized variable names whenever possible. Case Insensitive. strongly advised to use Camel Case. + Instance string `json:"instance,omitempty" validate:"omitempty,max=50"` // Name of instance in case the variable exists as multiple instances. Case Insensitive. strongly advised to use Camel Case. +} + +// ComponentVariable is used to report components, variables and variable attributes and characteristics. +type ComponentVariable struct { + Component Component `json:"component" validate:"required"` // Component for which a report of Variable is requested. + Variable Variable `json:"variable" validate:"required"` // Variable for which report is requested. +} + +// Attribute is an enumeration type used when requesting a variable value. +type Attribute string + +const ( + AttributeActual Attribute = "Actual" // The actual value of the variable. + AttributeTarget Attribute = "Target" // The target value for this variable. + AttributeMinSet Attribute = "MinSet" // The minimal allowed value for this variable. + AttributeMaxSet Attribute = "MaxSet" // The maximum allowed value for this variable +) + +func isValidAttribute(fl validator.FieldLevel) bool { + purposeType := Attribute(fl.Field().String()) + switch purposeType { + case AttributeActual, AttributeTarget, AttributeMinSet, AttributeMaxSet: + return true + default: + return false + } +} + +//TODO: remove SignatureMethod (obsolete from 2.0.1 onwards) + +// Enumeration of the method used to encode the meter value into binary data before applying the digital signature algorithm. +// If the EncodingMethod is set to Other, the CSMS MAY try to determine the encoding method from the encodedMeterValue field. +type EncodingMethod string + +const ( + EncodingOther EncodingMethod = "Other" // Encoding method is not included in the enumeration. + EncodingDLMSMessage EncodingMethod = "DLMS Message" // The data is encoded in a digitally signed DLMS message, as described in the DLMS Green Book 8. + EncodingCOSEMProtectedData EncodingMethod = "COSEM Protected Data" // The data is encoded according to the COSEM data protection methods, as described in the DLMS Blue Book 12. + EncodingEDL EncodingMethod = "EDL" // The data is encoded in the format used by EDL meters. +) + +func isValidEncodingMethod(fl validator.FieldLevel) bool { + encoding := EncodingMethod(fl.Field().String()) + switch encoding { + case EncodingCOSEMProtectedData, EncodingEDL, EncodingDLMSMessage, EncodingOther: + return true + default: + return false + } +} + +// Validator used for validating all OCPP 2.1 messages. +// Any additional custom validations must be added to this object for automatic validation. +var Validate = ocppj.Validate + +func init() { + _ = Validate.RegisterValidation("idTokenType21", isValidIdTokenType) + _ = Validate.RegisterValidation("genericDeviceModelStatus21", isValidGenericDeviceModelStatus) + _ = Validate.RegisterValidation("genericStatus21", isValidGenericStatus) + _ = Validate.RegisterValidation("hashAlgorithm21", isValidHashAlgorithmType) + _ = Validate.RegisterValidation("messageFormat21", isValidMessageFormatType) + _ = Validate.RegisterValidation("authorizationStatus21", isValidAuthorizationStatus) + _ = Validate.RegisterValidation("attribute21", isValidAttribute) + _ = Validate.RegisterValidation("chargingProfilePurpose21", isValidChargingProfilePurpose) + _ = Validate.RegisterValidation("chargingProfileKind21", isValidChargingProfileKind) + _ = Validate.RegisterValidation("recurrencyKind21", isValidRecurrencyKind) + _ = Validate.RegisterValidation("chargingRateUnit21", isValidChargingRateUnit) + _ = Validate.RegisterValidation("chargingLimitSource", isValidChargingLimitSource) + _ = Validate.RegisterValidation("remoteStartStopStatus21", isValidRemoteStartStopStatus) + _ = Validate.RegisterValidation("readingContext21", isValidReadingContext) + _ = Validate.RegisterValidation("measurand21", isValidMeasurand) + _ = Validate.RegisterValidation("phase21", isValidPhase) + _ = Validate.RegisterValidation("location21", isValidLocation) + _ = Validate.RegisterValidation("signatureMethod21", isValidSignatureMethod) + _ = Validate.RegisterValidation("encodingMethod21", isValidEncodingMethod) + _ = Validate.RegisterValidation("certificateSigningUse21", isValidCertificateSigningUse) + _ = Validate.RegisterValidation("certificateUse21", isValidCertificateUse) + _ = Validate.RegisterValidation("15118EVCertificate21", isValidCertificate15118EVStatus) + _ = Validate.RegisterValidation("costKind21", isValidCostKind) + _ = Validate.RegisterValidation("operationMode21", isValidOperationMode) + + Validate.RegisterStructValidation(isValidIdToken, IdToken{}) + Validate.RegisterStructValidation(isValidGroupIdToken, GroupIdToken{}) +} diff --git a/ocpp2.1/v21.go b/ocpp2.1/v21.go new file mode 100644 index 00000000..35ca72aa --- /dev/null +++ b/ocpp2.1/v21.go @@ -0,0 +1,457 @@ +// The package contains an implementation of the OCPP 2.1 communication protocol between a Charging Station and an Charging Station Management System in an EV charging infrastructure. +package ocpp21 + +import ( + "crypto/tls" + "net" + + "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/authorization" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/data" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/diagnostics" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/display" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/iso15118" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/meter" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/remotecontrol" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/security" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/tariffcost" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/transactions" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/lorenzodonini/ocpp-go/ws" +) + +type ChargingStationConnection interface { + ID() string + RemoteAddr() net.Addr + TLSConnectionState() *tls.ConnectionState +} + +type ( + ChargingStationValidationHandler ws.CheckClientHandler + ChargingStationConnectionHandler func(chargePoint ChargingStationConnection) +) + +// -------------------- v2.1 Charging Station -------------------- + +// A Charging Station represents the physical system where an EV can be charged. +// You can instantiate a default Charging Station struct by calling NewChargingStation. +// +// The logic for incoming messages needs to be implemented, and message handlers need to be registered with the charging station: +// +// handler := &ChargingStationHandler{} // Custom struct +// chargingStation.SetAuthorizationHandler(handler) +// chargingStation.SetProvisioningHandler(handler) +// // set more handlers... +// +// Refer to the ChargingStationHandler interface of each profile for the implementation requirements. +// +// If a handler for a profile is not set, the OCPP library will reply to incoming messages for that profile with a NotImplemented error. +// +// A charging station can be started and stopped using the Start and Stop functions. +// While running, messages can be sent to the CSMS by calling the Charging Station's functions, e.g. +// +// bootConf, err := chargingStation.BootNotification(BootReasonPowerUp, "model1", "vendor1") +// +// All messages are synchronous blocking, and return either the response from the CSMS or an error. +// To send asynchronous messages and avoid blocking the calling thread, refer to SendRequestAsync. +type ChargingStation interface { + // Sends a BootNotificationRequest to the CSMS, along with information about the charging station. + BootNotification(reason provisioning.BootReason, model string, chargePointVendor string, props ...func(request *provisioning.BootNotificationRequest)) (*provisioning.BootNotificationResponse, error) + // Requests explicit authorization to the CSMS, provided a valid IdToken (typically the customer's). The CSMS may either authorize or reject the token. + Authorize(idToken string, tokenType types.IdTokenType, props ...func(request *authorization.AuthorizeRequest)) (*authorization.AuthorizeResponse, error) + // Notifies the CSMS, that a previously set charging limit was cleared. + ClearedChargingLimit(chargingLimitSource types.ChargingLimitSourceType, props ...func(request *smartcharging.ClearedChargingLimitRequest)) (*smartcharging.ClearedChargingLimitResponse, error) + // Performs a custom data transfer to the CSMS. The message payload is not pre-defined and must be supported by the CSMS. Every vendor may implement their own proprietary logic for this message. + DataTransfer(vendorId string, props ...func(request *data.DataTransferRequest)) (*data.DataTransferResponse, error) + // Notifies the CSMS of a status change during a firmware update procedure (download, installation). + FirmwareStatusNotification(status firmware.FirmwareStatus, props ...func(request *firmware.FirmwareStatusNotificationRequest)) (*firmware.FirmwareStatusNotificationResponse, error) + // Requests a new certificate, required for an ISO 15118 EV, from the CSMS. + Get15118EVCertificate(schemaVersion string, action iso15118.CertificateAction, exiRequest string, props ...func(request *iso15118.Get15118EVCertificateRequest)) (*iso15118.Get15118EVCertificateResponse, error) + // Requests the CSMS to provide OCSP certificate status for the charging station's 15118 certificates. + GetCertificateStatus(ocspRequestData types.OCSPRequestDataType, props ...func(request *iso15118.GetCertificateStatusRequest)) (*iso15118.GetCertificateStatusResponse, error) + // Notifies the CSMS that the Charging Station is still alive. The response is used for time synchronization purposes. + Heartbeat(props ...func(request *availability.HeartbeatRequest)) (*availability.HeartbeatResponse, error) + // Updates the CSMS with the current log upload status. + LogStatusNotification(status diagnostics.UploadLogStatus, requestID int, props ...func(request *diagnostics.LogStatusNotificationRequest)) (*diagnostics.LogStatusNotificationResponse, error) + // Sends electrical meter values, not related to a transaction, to the CSMS. This message is deprecated and will be replaced by Device Management Monitoring events. + MeterValues(evseID int, meterValues []types.MeterValue, props ...func(request *meter.MeterValuesRequest)) (*meter.MeterValuesResponse, error) + // Informs the CSMS of a charging schedule or charging limit imposed by an External Control System on the Charging Station with ongoing transaction(s). + NotifyChargingLimit(chargingLimit smartcharging.ChargingLimit, props ...func(request *smartcharging.NotifyChargingLimitRequest)) (*smartcharging.NotifyChargingLimitResponse, error) + // Notifies the CSMS with raw customer data, previously requested by the CSMS (see CustomerInformationFeature). + NotifyCustomerInformation(data string, seqNo int, generatedAt types.DateTime, requestID int, props ...func(request *diagnostics.NotifyCustomerInformationRequest)) (*diagnostics.NotifyCustomerInformationResponse, error) + // Notifies the CSMS of the display messages currently configured on the Charging Station. + NotifyDisplayMessages(requestID int, props ...func(request *display.NotifyDisplayMessagesRequest)) (*display.NotifyDisplayMessagesResponse, error) + // Forwards the charging needs of an EV to the CSMS. + NotifyEVChargingNeeds(evseID int, chargingNeeds smartcharging.ChargingNeeds, props ...func(request *smartcharging.NotifyEVChargingNeedsRequest)) (*smartcharging.NotifyEVChargingNeedsResponse, error) + // Communicates the charging schedule as calculated by the EV to the CSMS. + NotifyEVChargingSchedule(timeBase *types.DateTime, evseID int, schedule types.ChargingSchedule, props ...func(request *smartcharging.NotifyEVChargingScheduleRequest)) (*smartcharging.NotifyEVChargingScheduleResponse, error) + // Notifies the CSMS about monitoring events. + NotifyEvent(generatedAt *types.DateTime, seqNo int, eventData []diagnostics.EventData, props ...func(request *diagnostics.NotifyEventRequest)) (*diagnostics.NotifyEventResponse, error) + // Sends a monitoring report to the CSMS, according to parameters specified in the GetMonitoringReport request, previously sent by the CSMS. + NotifyMonitoringReport(requestID int, seqNo int, generatedAt *types.DateTime, monitorData []diagnostics.MonitoringData, props ...func(request *diagnostics.NotifyMonitoringReportRequest)) (*diagnostics.NotifyMonitoringReportResponse, error) + // Sends a base report to the CSMS, according to parameters specified in the GetBaseReport request, previously sent by the CSMS. + NotifyReport(requestID int, generatedAt *types.DateTime, seqNo int, props ...func(request *provisioning.NotifyReportRequest)) (*provisioning.NotifyReportResponse, error) + // Notifies the CSMS about the current progress of a PublishFirmware operation. + PublishFirmwareStatusNotification(status firmware.PublishFirmwareStatus, props ...func(request *firmware.PublishFirmwareStatusNotificationRequest)) (*firmware.PublishFirmwareStatusNotificationResponse, error) + // Reports charging profiles installed in the Charging Station, as requested previously by the CSMS. + ReportChargingProfiles(requestID int, chargingLimitSource types.ChargingLimitSourceType, evseID int, chargingProfile []types.ChargingProfile, props ...func(request *smartcharging.ReportChargingProfilesRequest)) (*smartcharging.ReportChargingProfilesResponse, error) + // Notifies the CSMS about a reservation status having changed (i.e. the reservation has expired) + ReservationStatusUpdate(reservationID int, status reservation.ReservationUpdateStatus, props ...func(request *reservation.ReservationStatusUpdateRequest)) (*reservation.ReservationStatusUpdateResponse, error) + // Informs the CSMS about critical security events. + SecurityEventNotification(typ string, timestamp *types.DateTime, props ...func(request *security.SecurityEventNotificationRequest)) (*security.SecurityEventNotificationResponse, error) + // Requests the CSMS to issue a new certificate. + SignCertificate(csr string, props ...func(request *security.SignCertificateRequest)) (*security.SignCertificateResponse, error) + // Informs the CSMS about a connector status change. + StatusNotification(timestamp *types.DateTime, status availability.ConnectorStatus, evseID int, connectorID int, props ...func(request *availability.StatusNotificationRequest)) (*availability.StatusNotificationResponse, error) + // Sends information to the CSMS about a transaction, used for billing purposes. + TransactionEvent(t transactions.TransactionEvent, timestamp *types.DateTime, reason transactions.TriggerReason, seqNo int, info transactions.Transaction, props ...func(request *transactions.TransactionEventRequest)) (*transactions.TransactionEventResponse, error) + // Registers a handler for incoming security profile messages + SetSecurityHandler(handler security.ChargingStationHandler) + // Registers a handler for incoming provisioning profile messages + SetProvisioningHandler(handler provisioning.ChargingStationHandler) + // Registers a handler for incoming authorization profile messages + SetAuthorizationHandler(handler authorization.ChargingStationHandler) + // Registers a handler for incoming local authorization list profile messages + SetLocalAuthListHandler(handler localauth.ChargingStationHandler) + // Registers a handler for incoming transactions profile messages + SetTransactionsHandler(handler transactions.ChargingStationHandler) + // Registers a handler for incoming remote control profile messages + SetRemoteControlHandler(handler remotecontrol.ChargingStationHandler) + // Registers a handler for incoming availability profile messages + SetAvailabilityHandler(handler availability.ChargingStationHandler) + // Registers a handler for incoming reservation profile messages + SetReservationHandler(handler reservation.ChargingStationHandler) + // Registers a handler for incoming tariff and cost profile messages + SetTariffCostHandler(handler tariffcost.ChargingStationHandler) + // Registers a handler for incoming meter profile messages + SetMeterHandler(handler meter.ChargingStationHandler) + // Registers a handler for incoming smart charging messages + SetSmartChargingHandler(handler smartcharging.ChargingStationHandler) + // Registers a handler for incoming firmware management messages + SetFirmwareHandler(handler firmware.ChargingStationHandler) + // Registers a handler for incoming ISO15118 management messages + SetISO15118Handler(handler iso15118.ChargingStationHandler) + // Registers a handler for incoming diagnostics messages + SetDiagnosticsHandler(handler diagnostics.ChargingStationHandler) + // Registers a handler for incoming display messages + SetDisplayHandler(handler display.ChargingStationHandler) + // Registers a handler for incoming data transfer messages + SetDataHandler(handler data.ChargingStationHandler) + // Sends a request to the CSMS. + // The CSMS will respond with a confirmation, or with an error if the request was invalid or could not be processed. + // In case of network issues (i.e. the remote host couldn't be reached), the function also returns an error. + // + // The request is synchronous blocking. + SendRequest(request ocpp.Request) (ocpp.Response, error) + // Sends an asynchronous request to the CSMS. + // The CSMS will respond with a confirmation message, or with an error if the request was invalid or could not be processed. + // This result is propagated via a callback, called asynchronously. + // + // In case of network issues (i.e. the remote host couldn't be reached), the function returns an error directly. In this case, the callback is never invoked. + SendRequestAsync(request ocpp.Request, callback func(confirmation ocpp.Response, protoError error)) error + // Connects to the CSMS and starts the charging station routine. + // The function doesn't block and returns right away, after having attempted to open a connection to the CSMS. + // If the connection couldn't be opened, an error is returned. + // + // Optional client options must be set before calling this function. Refer to NewChargingStation. + // + // No auto-reconnect logic is implemented as of now, but is planned for the future. + Start(csmsUrl string) error + + // Connects to the CSMS and starts the charging station routine, it retries if first attempt fails. + // The function doesn't block and returns right away, after having attempted to open a connection to the CSMS. + // If the connection couldn't be opened, it retries. + // + // Optional client options must be set before calling this function. Refer to NewChargingStation. + StartWithRetries(csmsUrl string) + // 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 +} + +// Creates a new OCPP 2.0 charging station client. +// The id parameter is required to uniquely identify the charge point. +// +// The endpoint and client parameters may be omitted, in order to use a default configuration: +// +// chargingStation := NewChargingStation("someUniqueId", nil, nil) +// +// Additional networking parameters (e.g. TLS or proxy configuration) may be passed, by creating a custom client. +// Here is an example for a client using TLS configuration with a self-signed certificate: +// +// certPool := x509.NewCertPool() +// data, err := os.ReadFile("serverSelfSignedCertFilename") +// if err != nil { +// log.Fatal(err) +// } +// ok = certPool.AppendCertsFromPEM(data) +// if !ok { +// log.Fatal("couldn't parse PEM certificate") +// } +// cs := NewChargingStation("someUniqueId", nil, ws.NewClient(ws.WithClientTLSConfig(&tls.Config{ +// RootCAs: certPool, +// })) +// +// For more advanced options, or if a custom networking/occpj layer is required, +// please refer to ocppj.Client and ws.Client. +func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) ChargingStation { + if client == nil { + client = ws.NewClient() + } + client.SetRequestedSubProtocol(types.V2Subprotocol) + + if endpoint == nil { + dispatcher := ocppj.NewDefaultClientDispatcher(ocppj.NewFIFOClientQueue(0)) + endpoint = ocppj.NewClient(id, client, dispatcher, nil, authorization.Profile, availability.Profile, data.Profile, diagnostics.Profile, display.Profile, firmware.Profile, iso15118.Profile, localauth.Profile, meter.Profile, provisioning.Profile, remotecontrol.Profile, reservation.Profile, security.Profile, smartcharging.Profile, tariffcost.Profile, transactions.Profile) + } + endpoint.SetDialect(ocpp.V2) + + cs := chargingStation{ + client: endpoint, + responseHandler: make(chan ocpp.Response, 1), + errorHandler: make(chan error, 1), + callbacks: callbackqueue.New(), + } + + // Callback invoked by dispatcher, whenever a queued request is canceled, due to timeout. + endpoint.SetOnRequestCanceled(cs.onRequestTimeout) + + cs.client.SetResponseHandler(func(confirmation ocpp.Response, requestId string) { + cs.responseHandler <- confirmation + }) + cs.client.SetErrorHandler(func(err *ocpp.Error, details interface{}) { + cs.errorHandler <- err + }) + cs.client.SetRequestHandler(cs.handleIncomingRequest) + return &cs +} + +// -------------------- v2.0 CSMS -------------------- + +// A Charging Station Management System (CSMS) manages Charging Stations and has the information for authorizing Management Users for using its Charging Stations. +// You can instantiate a default CSMS struct by calling the NewCSMS function. +// +// The logic for handling incoming messages needs to be implemented, and message handlers need to be registered with the CSMS: +// +// handler := &CSMSHandler{} // Custom struct +// csms.SetAuthorizationHandler(handler) +// csms.SetProvisioningHandler(handler) +// // set more handlers... +// +// Refer to the CSMSHandler interface of each profile for the implementation requirements. +// +// If a handler for a profile is not set, the OCPP library will reply to incoming messages for that profile with a NotImplemented error. +// +// A CSMS can be started by using the Start function. +// To be notified of incoming (dis)connections from charging stations refer to the SetNewChargingStationHandler and SetChargingStationDisconnectedHandler functions. +// +// While running, messages can be sent to a Charging Station by calling the CSMS's functions, e.g.: +// +// callback := func(conf *ClearDisplayResponse, err error) { +// // handle the response... +// } +// clearDisplayConf, err := csms.ClearDisplay("cs0001", callback, 10) +// +// All messages are sent asynchronously and do not block the caller. +type CSMS interface { + // Cancel a pending reservation, provided the reservationId, on a charging station. + CancelReservation(clientId string, callback func(*reservation.CancelReservationResponse, error), reservationId int, props ...func(*reservation.CancelReservationRequest)) error + // Installs a new certificate (chain), signed by the CA, on the charging station. This typically follows a SignCertificate message, initiated by the charging station. + CertificateSigned(clientId string, callback func(*security.CertificateSignedResponse, error), CertificateSigned string, props ...func(*security.CertificateSignedRequest)) error + // Instructs a charging station to change its availability to the desired operational status. + ChangeAvailability(clientId string, callback func(*availability.ChangeAvailabilityResponse, error), operationalStatus availability.OperationalStatus, props ...func(*availability.ChangeAvailabilityRequest)) error + // Instructs a charging station to clear its current authorization cache. All authorization saved locally will be invalidated. + ClearCache(clientId string, callback func(*authorization.ClearCacheResponse, error), props ...func(*authorization.ClearCacheRequest)) error + // Instructs a charging station to clear some or all charging profiles, previously sent to the charging station. + ClearChargingProfile(clientId string, callback func(*smartcharging.ClearChargingProfileResponse, error), props ...func(request *smartcharging.ClearChargingProfileRequest)) error + // Removes a specific display message, currently configured in a charging station. + ClearDisplay(clientId string, callback func(*display.ClearDisplayResponse, error), id int, props ...func(*display.ClearDisplayRequest)) error + // Removes one or more monitoring settings from a charging station for the given variable IDs. + ClearVariableMonitoring(clientId string, callback func(*diagnostics.ClearVariableMonitoringResponse, error), id []int, props ...func(*diagnostics.ClearVariableMonitoringRequest)) error + // Instructs a charging station to display the updated current total cost of an ongoing transaction. + CostUpdated(clientId string, callback func(*tariffcost.CostUpdatedResponse, error), totalCost float64, transactionId string, props ...func(*tariffcost.CostUpdatedRequest)) error + // Instructs a charging station to send one or more reports, containing raw customer information. + CustomerInformation(clientId string, callback func(*diagnostics.CustomerInformationResponse, error), requestId int, report bool, clear bool, props ...func(*diagnostics.CustomerInformationRequest)) error + // Performs a custom data transfer to a charging station. The message payload is not pre-defined and must be supported by the charging station. Every vendor may implement their own proprietary logic for this message. + DataTransfer(clientId string, callback func(*data.DataTransferResponse, error), vendorId string, props ...func(*data.DataTransferRequest)) error + // Deletes a previously installed certificate on a charging station. + DeleteCertificate(clientId string, callback func(*iso15118.DeleteCertificateResponse, error), data types.CertificateHashData, props ...func(*iso15118.DeleteCertificateRequest)) error + // Requests a report from a charging station. The charging station will asynchronously send the report in chunks using NotifyReportRequest messages. + GetBaseReport(clientId string, callback func(*provisioning.GetBaseReportResponse, error), requestId int, reportBase provisioning.ReportBaseType, props ...func(*provisioning.GetBaseReportRequest)) error + // Request a charging station to report some or all installed charging profiles. The charging station will report these asynchronously using ReportChargingProfiles messages. + GetChargingProfiles(clientId string, callback func(*smartcharging.GetChargingProfilesResponse, error), chargingProfile smartcharging.ChargingProfileCriterion, props ...func(*smartcharging.GetChargingProfilesRequest)) error + // Requests a charging station to report the composite charging schedule for the indicated duration and evseID. + GetCompositeSchedule(clientId string, callback func(*smartcharging.GetCompositeScheduleResponse, error), duration int, evseId int, props ...func(*smartcharging.GetCompositeScheduleRequest)) error + // 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), 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. + GetLog(clientId string, callback func(*diagnostics.GetLogResponse, error), logType diagnostics.LogType, requestID int, logParameters diagnostics.LogParameters, props ...func(*diagnostics.GetLogRequest)) error + // Requests a report about configured monitoring settings per component and variable from a charging station. The reports will be uploaded asynchronously using NotifyMonitoringReport messages. + GetMonitoringReport(clientId string, callback func(*diagnostics.GetMonitoringReportResponse, error), props ...func(*diagnostics.GetMonitoringReportRequest)) error + // Requests a custom report about configured monitoring settings per criteria, component and variable from a charging station. The reports will be uploaded asynchronously using NotifyMonitoringReport messages. + GetReport(clientId string, callback func(*provisioning.GetReportResponse, error), props ...func(*provisioning.GetReportRequest)) error + // Asks a Charging Station whether it has transaction-related messages waiting to be delivered to the CSMS. When a transactionId is provided, only messages for a specific transaction are asked for. + GetTransactionStatus(clientId string, callback func(*transactions.GetTransactionStatusResponse, error), props ...func(*transactions.GetTransactionStatusRequest)) error + // Retrieves from a Charging Station the value of an attribute for one or more Variable of one or more Components. + GetVariables(clientId string, callback func(*provisioning.GetVariablesResponse, error), variableData []provisioning.GetVariableData, props ...func(*provisioning.GetVariablesRequest)) error + // Installs a new CA certificate on a Charging station. + InstallCertificate(clientId string, callback func(*iso15118.InstallCertificateResponse, error), certificateType types.CertificateUse, certificate string, props ...func(*iso15118.InstallCertificateRequest)) error + // 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.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.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. + SendLocalList(clientId string, callback func(*localauth.SendLocalListResponse, error), version int, updateType localauth.UpdateType, props ...func(request *localauth.SendLocalListRequest)) error + // Sends a charging profile to a charging station, to influence the power/current drawn by EVs. + SetChargingProfile(clientId string, callback func(*smartcharging.SetChargingProfileResponse, error), evseID int, chargingProfile *types.ChargingProfile, props ...func(request *smartcharging.SetChargingProfileRequest)) error + // Asks a charging station to configure a new display message, that should be displayed (in the future). + SetDisplayMessage(clientId string, callback func(*display.SetDisplayMessageResponse, error), message display.MessageInfo, props ...func(request *display.SetDisplayMessageRequest)) error + // Requests a charging station to activate a set of preconfigured monitoring settings, as denoted by the value of MonitoringBase. + SetMonitoringBase(clientId string, callback func(*diagnostics.SetMonitoringBaseResponse, error), monitoringBase diagnostics.MonitoringBase, props ...func(request *diagnostics.SetMonitoringBaseRequest)) error + // Restricts a Charging Station to reporting only monitoring events with a severity number lower than or equal to a certain severity. + SetMonitoringLevel(clientId string, callback func(*diagnostics.SetMonitoringLevelResponse, error), severity int, props ...func(request *diagnostics.SetMonitoringLevelRequest)) error + // Updates the connection details on a Charging Station. + SetNetworkProfile(clientId string, callback func(*provisioning.SetNetworkProfileResponse, error), configurationSlot int, connectionData provisioning.NetworkConnectionProfile, props ...func(request *provisioning.SetNetworkProfileRequest)) error + // Requests a Charging Station to set monitoring triggers on variables. + SetVariableMonitoring(clientId string, callback func(*diagnostics.SetVariableMonitoringResponse, error), data []diagnostics.SetMonitoringData, props ...func(request *diagnostics.SetVariableMonitoringRequest)) error + // Configures/changes the values of a set of variables on a charging station. + SetVariables(clientId string, callback func(*provisioning.SetVariablesResponse, error), data []provisioning.SetVariableData, props ...func(request *provisioning.SetVariablesRequest)) error + // Requests a Charging Station to send a charging station-initiated message. + TriggerMessage(clientId string, callback func(*remotecontrol.TriggerMessageResponse, error), requestedMessage remotecontrol.MessageTrigger, props ...func(request *remotecontrol.TriggerMessageRequest)) error + // Instructs the Charging Station to unlock a connector, to help out an EV-driver. + UnlockConnector(clientId string, callback func(*remotecontrol.UnlockConnectorResponse, error), evseID int, connectorID int, props ...func(request *remotecontrol.UnlockConnectorRequest)) error + // Instructs a Local Controller to stops serving a firmware update to connected Charging Stations. + UnpublishFirmware(clientId string, callback func(*firmware.UnpublishFirmwareResponse, error), checksum string, props ...func(request *firmware.UnpublishFirmwareRequest)) error + // Instructs a Charging Station to download and install a firmware update. + UpdateFirmware(clientId string, callback func(*firmware.UpdateFirmwareResponse, error), requestID int, firmware firmware.Firmware, props ...func(request *firmware.UpdateFirmwareRequest)) error + + // Registers a handler for incoming security profile messages. + SetSecurityHandler(handler security.CSMSHandler) + // Registers a handler for incoming provisioning profile messages. + SetProvisioningHandler(handler provisioning.CSMSHandler) + // Registers a handler for incoming authorization profile messages. + SetAuthorizationHandler(handler authorization.CSMSHandler) + // Registers a handler for incoming local authorization list profile messages. + SetLocalAuthListHandler(handler localauth.CSMSHandler) + // Registers a handler for incoming transactions profile messages + SetTransactionsHandler(handler transactions.CSMSHandler) + // Registers a handler for incoming remote control profile messages + SetRemoteControlHandler(handler remotecontrol.CSMSHandler) + // Registers a handler for incoming availability profile messages + SetAvailabilityHandler(handler availability.CSMSHandler) + // Registers a handler for incoming reservation profile messages + SetReservationHandler(handler reservation.CSMSHandler) + // Registers a handler for incoming tariff and cost profile messages + SetTariffCostHandler(handler tariffcost.CSMSHandler) + // Registers a handler for incoming meter profile messages + SetMeterHandler(handler meter.CSMSHandler) + // Registers a handler for incoming smart charging messages + SetSmartChargingHandler(handler smartcharging.CSMSHandler) + // Registers a handler for incoming firmware management messages + SetFirmwareHandler(handler firmware.CSMSHandler) + // Registers a handler for incoming ISO15118 management messages + SetISO15118Handler(handler iso15118.CSMSHandler) + // Registers a handler for incoming diagnostics messages + SetDiagnosticsHandler(handler diagnostics.CSMSHandler) + // Registers a handler for incoming display messages + SetDisplayHandler(handler display.CSMSHandler) + // 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) + // Sends an asynchronous request to a Charging Station, identified by the clientId. + // The charging station will respond with a confirmation message, or with an error if the request was invalid or could not be processed. + // This result is propagated via a callback, called asynchronously. + // In case of network issues (i.e. the remote host couldn't be reached), the function returns an error directly. In this case, the callback is never invoked. + SendRequestAsync(clientId string, request ocpp.Request, callback func(ocpp.Response, error)) error + // Starts running the CSMS on the specified port and URL. + // The central system runs as a daemon and handles incoming charge point connections and messages. + + // The function blocks forever, so it is suggested to wrap it in a goroutine, in case other functionality needs to be executed on the main program thread. + Start(listenPort int, listenPath string) + // Stops the CSMS, clearing all pending requests. + Stop() + // Errors returns a channel for error messages. If it doesn't exist it es created. + Errors() <-chan error +} + +// Creates a new OCPP 2.0 CSMS. +// +// The endpoint and client parameters may be omitted, in order to use a default configuration: +// +// csms := NewCSMS(nil, nil) +// +// It is recommended to use the default configuration, unless a custom networking / ocppj layer is required. +// The default dispatcher supports all implemented OCPP 2.0 features out-of-the-box. +// +// If you need a TLS server, you may use the following: +// +// csms := NewCSMS(nil, ws.NewServer(ws.WithServerTLSConfig("certificatePath", "privateKeyPath", nil))) +func NewCSMS(endpoint *ocppj.Server, server ws.Server) CSMS { + if server == nil { + server = ws.NewServer() + } + server.AddSupportedSubprotocol(types.V2Subprotocol) + if endpoint == nil { + endpoint = ocppj.NewServer(server, nil, nil, + authorization.Profile, + availability.Profile, + data.Profile, + diagnostics.Profile, + display.Profile, + firmware.Profile, + iso15118.Profile, + localauth.Profile, + meter.Profile, + provisioning.Profile, + remotecontrol.Profile, + reservation.Profile, + security.Profile, + smartcharging.Profile, + tariffcost.Profile, + transactions.Profile, + ) + } + cs := newCSMS(endpoint) + cs.server.SetRequestHandler(func(client ws.Channel, request ocpp.Request, requestId string, action string) { + cs.handleIncomingRequest(client, request, requestId, action) + }) + cs.server.SetResponseHandler(func(client ws.Channel, response ocpp.Response, requestId string) { + cs.handleIncomingResponse(client, response, requestId) + }) + cs.server.SetErrorHandler(func(client ws.Channel, err *ocpp.Error, details interface{}) { + cs.handleIncomingError(client, err, details) + }) + cs.server.SetCanceledRequestHandler(func(clientID string, requestID string, request ocpp.Request, err *ocpp.Error) { + cs.handleCanceledRequest(clientID, request, err) + }) + return &cs +} diff --git a/ocpp2.1/v2x/affr_signal.go b/ocpp2.1/v2x/affr_signal.go new file mode 100644 index 00000000..a0b73af0 --- /dev/null +++ b/ocpp2.1/v2x/affr_signal.go @@ -0,0 +1,57 @@ +package v2x + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- AFRRSignal (CSMS -> CS) -------------------- + +const AFRRSignal = "AFRRSignal" + +// The field definition of the AFRRSignalRequest request payload sent by the CSMS to the Charging Station. +type AFRRSignalRequest struct { + Timestamp *types.DateTime `json:"timestamp" validate:"required"` + Signal int `json:"signal" validate:"required"` +} + +// This field definition of the AFRRSignalResponse +type AFRRSignalResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty"` +} + +type AFRRSignalFeature struct{} + +func (f AFRRSignalFeature) GetFeatureName() string { + return AFRRSignal +} + +func (f AFRRSignalFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(AFRRSignalRequest{}) +} + +func (f AFRRSignalFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(AFRRSignalResponse{}) +} + +func (r AFRRSignalRequest) GetFeatureName() string { + return AFRRSignal +} + +func (c AFRRSignalResponse) GetFeatureName() string { + return AFRRSignal +} + +// Creates a new AFRRSignalRequest, containing all required fields. Optional fields may be set afterwards. +func NewAFRRSignalRequest(timestamp *types.DateTime, signal int) *AFRRSignalRequest { + return &AFRRSignalRequest{ + Timestamp: timestamp, + Signal: signal, + } +} + +// Creates a new NewAFFRSignalResponse, containing all required fields. Optional fields may be set afterwards. +func NewAFRRSignalResponse(status types.GenericStatus) *AFRRSignalResponse { + return &AFRRSignalResponse{Status: status} +} diff --git a/ocpp2.1/v2x/notify_allowed_energy_transfer.go b/ocpp2.1/v2x/notify_allowed_energy_transfer.go new file mode 100644 index 00000000..3c7e02b3 --- /dev/null +++ b/ocpp2.1/v2x/notify_allowed_energy_transfer.go @@ -0,0 +1,57 @@ +package v2x + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- NotifyAllowedEnergyTransfer (CSMS -> CS) -------------------- + +const NotifyAllowedEnergyTransfer = "NotifyAllowedEnergyTransfer" + +// The field definition of the NotifyAllowedEnergyTransferRequest request payload sent by the CSMS to the Charging Station. +type NotifyAllowedEnergyTransferRequest struct { + TransactionId string `json:"transactionId" validate:"required,max=36"` + AllowedEnergyTransfer []types.EnergyTransferMode `json:"allowedEnergyTransfer" validate:"required,energyTransferMode21"` +} + +// This field definition of the NotifyAllowedEnergyTransferResponse +type NotifyAllowedEnergyTransferResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo" validate:"omitempty,dive"` +} + +type NotifyAllowedEnergyTransferFeature struct{} + +func (f NotifyAllowedEnergyTransferFeature) GetFeatureName() string { + return NotifyAllowedEnergyTransfer +} + +func (f NotifyAllowedEnergyTransferFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyAllowedEnergyTransferRequest{}) +} + +func (f NotifyAllowedEnergyTransferFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyAllowedEnergyTransferResponse{}) +} + +func (r NotifyAllowedEnergyTransferRequest) GetFeatureName() string { + return NotifyAllowedEnergyTransfer +} + +func (c NotifyAllowedEnergyTransferResponse) GetFeatureName() string { + return NotifyAllowedEnergyTransfer +} + +// Creates a new NotifyAllowedEnergyTransferRequest, containing all required fields. Optional fields may be set afterwards. +func NewNotifyAllowedEnergyTransferRequest(transactionId string, allowedEnergyTransfers ...types.EnergyTransferMode) *NotifyAllowedEnergyTransferRequest { + return &NotifyAllowedEnergyTransferRequest{ + TransactionId: transactionId, + AllowedEnergyTransfer: allowedEnergyTransfers, + } +} + +// Creates a new NotifyAllowedEnergyTransferResponse, containing all required fields. Optional fields may be set afterwards. +func NewNotifyAllowedEnergyTransferResponse(status types.GenericStatus) *NotifyAllowedEnergyTransferResponse { + return &NotifyAllowedEnergyTransferResponse{Status: status} +} diff --git a/ocpp2.1/v2x/v2x.go b/ocpp2.1/v2x/v2x.go new file mode 100644 index 00000000..4eb2182e --- /dev/null +++ b/ocpp2.1/v2x/v2x.go @@ -0,0 +1,23 @@ +package v2x + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" +) + +// Needs to be implemented by a CSMS for handling messages part of the OCPP 2.1 V2X. +type CSMSHandler interface { +} + +// Needs to be implemented by Charging stations for handling messages part of the V2X. +type ChargingStationHandler interface { + OnAFRRSignal(chargingStationId string, request *AFRRSignalRequest) (*AFRRSignalResponse, error) + OnNotifyAllowedEnergyTransfer(chargingStationId string, request *NotifyAllowedEnergyTransferRequest) (*NotifyAllowedEnergyTransferResponse, error) +} + +const ProfileName = "V2X" + +var Profile = ocpp.NewProfile( + ProfileName, + AFRRSignalFeature{}, + NotifyAllowedEnergyTransferFeature{}, +) From 7bb906af2e3050b1956811c1e42a0e0ba7c0dc7e Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Mon, 23 Jun 2025 00:00:34 +0200 Subject: [PATCH 03/12] docs: initial docs version --- README.md | 6 ++- docs/ocpp-2.1.md | 127 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 docs/ocpp-2.1.md diff --git a/README.md b/README.md index 729ceb43..d956fdf6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Planned milestones and features: - [x] OCPP 1.6 Security extension (documentation available [here](docs/ocpp1.6-security-extension.md)) - [x] OCPP 2.0.1 (examples working, but will need more real-world testing) (documentation available [here](docs/ocpp-2.0.1.md)) +- [x] OCPP 2.1 (beta) (documentation available [here](docs/ocpp-2.1.md)) ### Features @@ -95,6 +96,7 @@ The websocket package supports configuring ping pong for both endpoints. By default, the client sends a ping every 54 seconds and waits for a pong for 60 seconds, before timing out. The values can be configured as follows: + ```go cfg := ws.NewClientTimeoutConfig() cfg.PingPeriod = 10 * time.Second @@ -102,8 +104,10 @@ cfg.PongWait = 20 * time.Second websocketClient.SetTimeoutConfig(cfg) ``` -By default, the server does not send out any pings and waits for a ping from the client for 60 seconds, before timing out. +By default, the server does not send out any pings and waits for a ping from the client for 60 seconds, before timing +out. To configure the server to send out pings, the `PingPeriod` and `PongWait` must be set to a value greater than 0: + ```go cfg := ws.NewServerTimeoutConfig() cfg.PingPeriod = 10 * time.Second diff --git a/docs/ocpp-2.1.md b/docs/ocpp-2.1.md new file mode 100644 index 00000000..1b8b53a2 --- /dev/null +++ b/docs/ocpp-2.1.md @@ -0,0 +1,127 @@ +## OCPP 2.1 Usage + +OCPP 2.1 is now supported (experimental) in this library. + +Requests and responses in OCPP 2.1 are handled the same way they were in v1.6 and 2.0.1. +There are additional message types on the websocket layer, which are handled automatically by the library. + +The notable change is that there are now significantly more supported messages and profiles (feature sets), +which also require their own handlers to be implemented. + +Below are very minimal setup code snippets, to get you started. +CSMS is now the equivalent of the Central System, +while the Charging Station is the new equivalent of a Charge Point. + +Refer to the [examples folder](../example/2.1) for a full working example. +More in-depth documentation for v2.1 will follow. + +### CSMS + +To start a CSMS instance, run the following: + +```go +import "github.com/lorenzodonini/ocpp-go/ocpp2.1" + +csms := ocpp2.NewCSMS(nil, nil) + +// Set callback handlers for connect/disconnect +csms.SetNewChargingStationHandler(func (chargingStation ocpp2.ChargingStationConnection) { +log.Printf("new charging station %v connected", chargingStation.ID()) +}) +csms.SetChargingStationDisconnectedHandler(func (chargingStation ocpp2.ChargingStationConnection) { +log.Printf("charging station %v disconnected", chargingStation.ID()) +}) + +// Set handler for profile callbacks +handler := &CSMSHandler{} +csms.SetAuthorizationHandler(handler) +csms.SetAvailabilityHandler(handler) +csms.SetDiagnosticsHandler(handler) +csms.SetFirmwareHandler(handler) +csms.SetLocalAuthListHandler(handler) +csms.SetMeterHandler(handler) +csms.SetProvisioningHandler(handler) +csms.SetRemoteControlHandler(handler) +csms.SetReservationHandler(handler) +csms.SetTariffCostHandler(handler) +csms.SetTransactionsHandler(handler) + + +// Start central system +listenPort := 8887 +log.Printf("starting CSMS") +csms.Start(listenPort, "/{ws}") // This call starts server in daemon mode and is blocking +log.Println("stopped CSMS") +``` + +#### Sending requests + +Similarly to v2.0.1, you may send requests using the simplified API, e.g. + +```go +err := csms.GetLocalListVersion(chargingStationID, myCallback) +if err != nil { +log.Printf("error sending message: %v", err) +} +``` + +Or you may build requests manually and send them using the asynchronous API. + +#### Docker image + +There is a Dockerfile and a docker image available upstream. + +### Charging Station + +To start a charging station instance, simply run the following: + +```go +chargingStationID := "cs0001" +csmsUrl = "ws://localhost:8887" +chargingStation := ocpp2.NewChargingStation(chargingStationID, nil, nil) + +// Set a handler for all callback functions +handler := &ChargingStationHandler{} +chargingStation.SetAvailabilityHandler(handler) +chargingStation.SetAuthorizationHandler(handler) +chargingStation.SetDataHandler(handler) +chargingStation.SetDiagnosticsHandler(handler) +chargingStation.SetDisplayHandler(handler) +chargingStation.SetFirmwareHandler(handler) +chargingStation.SetISO15118Handler(handler) +chargingStation.SetLocalAuthListHandler(handler) +chargingStation.SetProvisioningHandler(handler) +chargingStation.SetRemoteControlHandler(handler) +chargingStation.SetReservationHandler(handler) +chargingStation.SetSmartChargingHandler(handler) +chargingStation.SetTariffCostHandler(handler) +chargingStation.SetTransactionsHandler(handler) + +// Connects to CSMS +err := chargingStation.Start(csmsUrl) +if err != nil { +log.Println(err) +} else { +log.Printf("connected to CSMS at %v", csmsUrl) +mainRoutine() // ... your program logic goes here +} + +// Disconnect +chargingStation.Stop() +log.Println("disconnected from CSMS") +``` + +#### Sending requests + +Similarly to v2.0.1 you may send requests using the simplified API (recommended), e.g. + +```go +bootResp, err := chargingStation.BootNotification(provisioning.BootReasonPowerUp, "model1", "vendor1") +if err != nil { +log.Printf("error sending message: %v", err) +} else { +log.Printf("status: %v, interval: %v, current time: %v", bootResp.Status, bootResp.Interval, bootResp.CurrentTime.String()) +} +``` + +Or you may build requests manually and send them using either the synchronous or asynchronous API. From 8e3ae0ab3d1a9d4be3b5eb20ad1cc9ca13cab405 Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Mon, 23 Jun 2025 22:06:29 +0200 Subject: [PATCH 04/12] feat: event stream support --- .../adjust_periodic_event_stream.go | 60 +++++++++++++++++++ .../close_periodic_event_stream.go | 51 ++++++++++++++++ ocpp2.1/diagnostics/diagnostics.go | 15 +++++ .../diagnostics/get_periodic_event_stream.go | 55 +++++++++++++++++ .../notify_periodic_event_stream.go | 52 ++++++++++++++++ .../diagnostics/open_periodic_event_stream.go | 58 ++++++++++++++++++ .../diagnostics/set_variable_monitoring.go | 20 +++---- 7 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 ocpp2.1/diagnostics/adjust_periodic_event_stream.go create mode 100644 ocpp2.1/diagnostics/close_periodic_event_stream.go create mode 100644 ocpp2.1/diagnostics/get_periodic_event_stream.go create mode 100644 ocpp2.1/diagnostics/notify_periodic_event_stream.go create mode 100644 ocpp2.1/diagnostics/open_periodic_event_stream.go diff --git a/ocpp2.1/diagnostics/adjust_periodic_event_stream.go b/ocpp2.1/diagnostics/adjust_periodic_event_stream.go new file mode 100644 index 00000000..9c348af4 --- /dev/null +++ b/ocpp2.1/diagnostics/adjust_periodic_event_stream.go @@ -0,0 +1,60 @@ +package diagnostics + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "reflect" +) + +// -------------------- Adjust Periodic EventStream (CSMS -> CS) -------------------- + +const AdjustPeriodicEventStream = "ClosePeriodicEventStream" + +// The field definition of the AdjustPeriodicEventStreamRequest request payload sent by the CSMS to the Charging Station. +type AdjustPeriodicEventStreamRequest struct { + Id int `json:"id" validate:"required,gte=0"` + Params PeriodicEventStreamParams `json:"params" validate:"required,dive"` +} + +// This field definition of the AdjustPeriodicEventStream response payload, sent by the Charging Station to the CSMS in response to a AdjustPeriodicEventStreamRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type AdjustPeriodicEventStreamResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +type AdjustPeriodicEventStreamFeature struct{} + +func (f AdjustPeriodicEventStreamFeature) GetFeatureName() string { + return AdjustPeriodicEventStream +} + +func (f AdjustPeriodicEventStreamFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(AdjustPeriodicEventStreamRequest{}) +} + +func (f AdjustPeriodicEventStreamFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(AdjustPeriodicEventStreamResponse{}) +} + +func (r AdjustPeriodicEventStreamRequest) GetFeatureName() string { + return AdjustPeriodicEventStream +} + +func (c AdjustPeriodicEventStreamResponse) GetFeatureName() string { + return AdjustPeriodicEventStream +} + +// Creates a new AdjustPeriodicEventStreamRequest, containing all required fields. There are no optional fields for this message. +func NewAdjustPeriodicEventStreamsRequest(id int, params PeriodicEventStreamParams) *AdjustPeriodicEventStreamRequest { + return &AdjustPeriodicEventStreamRequest{ + Id: id, + Params: params, + } +} + +// Creates a new AdjustPeriodicEventStreamResponse, which doesn't contain any required or optional fields. +func NewAdjustPeriodicEventStreamResponse(status types.GenericStatus) *AdjustPeriodicEventStreamResponse { + return &AdjustPeriodicEventStreamResponse{ + Status: status, + } +} diff --git a/ocpp2.1/diagnostics/close_periodic_event_stream.go b/ocpp2.1/diagnostics/close_periodic_event_stream.go new file mode 100644 index 00000000..a74721de --- /dev/null +++ b/ocpp2.1/diagnostics/close_periodic_event_stream.go @@ -0,0 +1,51 @@ +package diagnostics + +import "reflect" + +// -------------------- Close Periodic EventStream (CSMS -> CS) -------------------- + +const ClosePeriodicEventStream = "ClosePeriodicEventStream" + +// The field definition of the ClosePeriodicEventStreamRequest request payload sent by the CSMS to the Charging Station. +type ClosePeriodicEventStreamRequest struct { + Id int `json:"id" validate:"required,gte=0"` +} + +// This field definition of the ClosePeriodicEventStream response payload, sent by the Charging Station to the CSMS in response to a ClosePeriodicEventStreamRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type ClosePeriodicEventStreamResponse struct { +} + +type ClosePeriodicEventStreamFeature struct{} + +func (f ClosePeriodicEventStreamFeature) GetFeatureName() string { + return ClosePeriodicEventStream +} + +func (f ClosePeriodicEventStreamFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(ClosePeriodicEventStreamRequest{}) +} + +func (f ClosePeriodicEventStreamFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(ClosePeriodicEventStreamResponse{}) +} + +func (r ClosePeriodicEventStreamRequest) GetFeatureName() string { + return ClosePeriodicEventStream +} + +func (c ClosePeriodicEventStreamResponse) GetFeatureName() string { + return ClosePeriodicEventStream +} + +// Creates a new ClosePeriodicEventStreamRequest, containing all required fields. There are no optional fields for this message. +func NewClosePeriodicEventStreamsRequest(id int, params PeriodicEventStreamParams) *ClosePeriodicEventStreamRequest { + return &ClosePeriodicEventStreamRequest{ + Id: id, + } +} + +// Creates a new ClosePeriodicEventStreamResponse, which doesn't contain any required or optional fields. +func NewClosePeriodicEventStreamResponse() *ClosePeriodicEventStreamResponse { + return &ClosePeriodicEventStreamResponse{} +} diff --git a/ocpp2.1/diagnostics/diagnostics.go b/ocpp2.1/diagnostics/diagnostics.go index 78f3d080..f1a422bd 100644 --- a/ocpp2.1/diagnostics/diagnostics.go +++ b/ocpp2.1/diagnostics/diagnostics.go @@ -13,6 +13,12 @@ type CSMSHandler interface { OnNotifyEvent(chargingStationID string, request *NotifyEventRequest) (response *NotifyEventResponse, err error) // OnNotifyMonitoringReport is called on the CSMS whenever a NotifyMonitoringReportRequest is received from a Charging Station. OnNotifyMonitoringReport(chargingStationID string, request *NotifyMonitoringReportRequest) (response *NotifyMonitoringReportResponse, err error) + // OnOpenPeriodicEventStream is called on the CSMS whenever a OpenPeriodicEventStreamRequest is received from a Charging Station. + OnOpenPeriodicEventStream(chargingStationID string, request *OpenPeriodicEventStreamRequest) (response *OpenPeriodicEventStreamResponse, err error) + // OnClosePeriodicEventStream is called on the CSMS whenever a ClosePeriodicEventStreamRequest is received from a Charging Station. + OnClosePeriodicEventStream(chargingStationID string, request *ClosePeriodicEventStreamRequest) (response *ClosePeriodicEventStreamResponse, err error) + // OnNotifyPeriodicEventStream is called on the CSMS whenever a NotifyPeriodicEventStream is received from a Charging Station. It requires no response. + OnNotifyPeriodicEventStream(chargingStationID string, request *NotifyPeriodicEventStream) } // Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 Diagnostics profile. @@ -31,6 +37,10 @@ type ChargingStationHandler interface { OnSetMonitoringLevel(request *SetMonitoringLevelRequest) (response *SetMonitoringLevelResponse, err error) // OnSetVariableMonitoring is called on a charging station whenever a SetVariableMonitoringRequest is received from the CSMS. OnSetVariableMonitoring(request *SetVariableMonitoringRequest) (response *SetVariableMonitoringResponse, err error) + // OnGetPeriodicEventStream is called on a charging station whenever a GetPeriodicEventStreamRequest is received from the CSMS. + OnGetPeriodicEventStream(request *GetPeriodicEventStreamRequest) (response *GetPeriodicEventStreamResponse, err error) + // OnAdjustPeriodicEventStream is called on a charging station whenever an AdjustPeriodicEventStreamRequest is received from the CSMS. + OnAdjustPeriodicEventStream(request *AdjustPeriodicEventStreamRequest) (response *AdjustPeriodicEventStreamResponse, err error) } const ProfileName = "Diagnostics" @@ -48,4 +58,9 @@ var Profile = ocpp.NewProfile( SetMonitoringBaseFeature{}, SetMonitoringLevelFeature{}, SetVariableMonitoringFeature{}, + OpenPeriodicEventStreamFeature{}, + ClosePeriodicEventStreamFeature{}, + GetPeriodicEventStreamFeature{}, + AdjustPeriodicEventStreamFeature{}, + NotifyPeriodicEventStreamFeature{}, ) diff --git a/ocpp2.1/diagnostics/get_periodic_event_stream.go b/ocpp2.1/diagnostics/get_periodic_event_stream.go new file mode 100644 index 00000000..d731749b --- /dev/null +++ b/ocpp2.1/diagnostics/get_periodic_event_stream.go @@ -0,0 +1,55 @@ +package diagnostics + +import "reflect" + +// -------------------- Get Periodic EventStream (CSMS -> CS) -------------------- + +const GetPeriodicEventStream = "GetPeriodicEventStream" + +// The field definition of the GetPeriodicEventStreamRequest request payload sent by the CSMS to the Charging Station. +type GetPeriodicEventStreamRequest struct { +} + +// This field definition of the GetPeriodicEventStream response payload, sent by the Charging Station to the CSMS in response to a GetPeriodicEventStreamRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type GetPeriodicEventStreamResponse struct { + ConstantStreamData []ConstantStreamData `json:"constantStreamData,omitempty" validate:"omitempty,dive"` +} + +type ConstantStreamData struct { + Id int `json:"id" validate:"required,gte=0"` + VariableMonitoringId int `json:"variableMonitoringId" validate:"required,gte=0"` + Params PeriodicEventStreamParams `json:"params" validate:"required,dive"` +} + +type GetPeriodicEventStreamFeature struct{} + +func (f GetPeriodicEventStreamFeature) GetFeatureName() string { + return GetPeriodicEventStream +} + +func (f GetPeriodicEventStreamFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(GetPeriodicEventStreamRequest{}) +} + +func (f GetPeriodicEventStreamFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(GetPeriodicEventStreamResponse{}) +} + +func (r GetPeriodicEventStreamRequest) GetFeatureName() string { + return GetPeriodicEventStream +} + +func (c GetPeriodicEventStreamResponse) GetFeatureName() string { + return GetPeriodicEventStream +} + +// Creates a new GetPeriodicEventStreamRequest, containing all required fields. There are no optional fields for this message. +func NewGetPeriodicEventStreamsRequest() *GetPeriodicEventStreamRequest { + return &GetPeriodicEventStreamRequest{} +} + +// Creates a new GetPeriodicEventStreamResponse, which doesn't contain any required or optional fields. +func NewGetPeriodicEventStreamResponse() *GetPeriodicEventStreamResponse { + return &GetPeriodicEventStreamResponse{} +} diff --git a/ocpp2.1/diagnostics/notify_periodic_event_stream.go b/ocpp2.1/diagnostics/notify_periodic_event_stream.go new file mode 100644 index 00000000..d74362f5 --- /dev/null +++ b/ocpp2.1/diagnostics/notify_periodic_event_stream.go @@ -0,0 +1,52 @@ +package diagnostics + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "reflect" +) + +const ( + NotifyPeriodicEventStreamFeat = "NotifyPeriodicEventStream" +) + +// The field definition of the NotifyPeriodicEventStream request payload sent by the Charging station to the CSMS. +type NotifyPeriodicEventStream struct { + ID int `json:"id" validate:"required,gte=0"` + Pending int `json:"pending" validate:"required,gte=0"` + BaseTime types.DateTime `json:"baseTime" validate:"required"` + Data []StreamDataElement `json:"data" validate:"required,dive"` // A list of StreamDataElements, each containing a stream of data. +} + +type StreamDataElement struct { + T float64 `json:"t" validate:"required"` + V string `json:"v" validate:"required,max=2500"` +} + +// Note: This feature does not have a response. This needs to be reflected in the websocket layer. +type NotifyPeriodicEventStreamFeature struct{} + +func (f NotifyPeriodicEventStreamFeature) GetFeatureName() string { + return NotifyPeriodicEventStreamFeat +} + +func (f NotifyPeriodicEventStreamFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(NotifyPeriodicEventStream{}) +} + +func (f NotifyPeriodicEventStreamFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(NotifyPeriodicEventStream{}) +} + +func (r NotifyPeriodicEventStream) GetFeatureName() string { + return NotifyPeriodicEventStreamFeat +} + +// Creates a new NotifyPeriodicEventStream, containing all required fields. Additional optional fields may be set afterwards. +func NewNotifyPeriodicEventStream(id, pending int, baseTime types.DateTime, data []StreamDataElement) *NotifyPeriodicEventStream { + return &NotifyPeriodicEventStream{ + ID: id, + Pending: pending, + BaseTime: baseTime, + Data: data, + } +} diff --git a/ocpp2.1/diagnostics/open_periodic_event_stream.go b/ocpp2.1/diagnostics/open_periodic_event_stream.go new file mode 100644 index 00000000..c1ca2bd8 --- /dev/null +++ b/ocpp2.1/diagnostics/open_periodic_event_stream.go @@ -0,0 +1,58 @@ +package diagnostics + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "reflect" +) + +// -------------------- Open Periodic EventStream (CSMS -> CS) -------------------- + +const OpenPeriodicEventStream = "OpenPeriodicEventStream" + +// The field definition of the OpenPeriodicEventStreamRequest request payload sent by the CSMS to the Charging Station. +type OpenPeriodicEventStreamRequest struct { + ConstantStreamData ConstantStreamData `json:"constantStreamData" validate:"required,dive"` +} + +// This field definition of the OpenPeriodicEventStream response payload, sent by the Charging Station to the CSMS in response to a OpenPeriodicEventStreamRequest. +// In case the request was invalid, or couldn't be processed, an error will be sent instead. +type OpenPeriodicEventStreamResponse struct { + Status types.GenericStatus `json:"status" validate:"required,genericStatus21"` + StatusInfo *types.StatusInfo `json:"statusInfo,omitempty" validate:"omitempty,dive"` +} + +type OpenPeriodicEventStreamFeature struct{} + +func (f OpenPeriodicEventStreamFeature) GetFeatureName() string { + return OpenPeriodicEventStream +} + +func (f OpenPeriodicEventStreamFeature) GetRequestType() reflect.Type { + return reflect.TypeOf(OpenPeriodicEventStreamRequest{}) +} + +func (f OpenPeriodicEventStreamFeature) GetResponseType() reflect.Type { + return reflect.TypeOf(OpenPeriodicEventStreamResponse{}) +} + +func (r OpenPeriodicEventStreamRequest) GetFeatureName() string { + return OpenPeriodicEventStream +} + +func (c OpenPeriodicEventStreamResponse) GetFeatureName() string { + return OpenPeriodicEventStream +} + +// Creates a new OpenPeriodicEventStreamRequest, containing all required fields. There are no optional fields for this message. +func NewOpenPeriodicEventStreamsRequest(data ConstantStreamData) *OpenPeriodicEventStreamRequest { + return &OpenPeriodicEventStreamRequest{ + ConstantStreamData: data, + } +} + +// Creates a new OpenPeriodicEventStreamResponse, which doesn't contain any required or optional fields. +func NewOpenPeriodicEventStreamResponse(status types.GenericStatus) *OpenPeriodicEventStreamResponse { + return &OpenPeriodicEventStreamResponse{ + Status: status, + } +} diff --git a/ocpp2.1/diagnostics/set_variable_monitoring.go b/ocpp2.1/diagnostics/set_variable_monitoring.go index 0c5d9d7a..63657dd2 100644 --- a/ocpp2.1/diagnostics/set_variable_monitoring.go +++ b/ocpp2.1/diagnostics/set_variable_monitoring.go @@ -35,19 +35,19 @@ func isValidSetMonitoringStatus(fl validator.FieldLevel) bool { // Hold parameters of a SetVariableMonitoring request. type SetMonitoringData struct { - ID *int `json:"id,omitempty" validate:"omitempty"` // An id SHALL only be given to replace an existing monitor. The Charging Station handles the generation of id’s for new monitors. - Transaction bool `json:"transaction,omitempty"` // Monitor only active when a transaction is ongoing on a component relevant to this transaction. - Value float64 `json:"value"` // Value for threshold or delta monitoring. For Periodic or PeriodicClockAligned this is the interval in seconds. - Type MonitorType `json:"type" validate:"required,monitorType"` // The type of this monitor, e.g. a threshold, delta or periodic monitor. - Severity int `json:"severity" validate:"min=0,max=9"` // The severity that will be assigned to an event that is triggered by this monitor. The severity range is 0-9, with 0 as the highest and 9 as the lowest severity level. - Component types.Component `json:"component" validate:"required"` // Component for which monitor is set. - Variable types.Variable `json:"variable" validate:"required"` // Variable for which monitor is set. - PeriodicEventStream PeriodicEventStreamParams `json:"periodicEventStream,omitempty" validate:"omitempty,dive"` + ID *int `json:"id,omitempty" validate:"omitempty"` // An id SHALL only be given to replace an existing monitor. The Charging Station handles the generation of id’s for new monitors. + Transaction bool `json:"transaction,omitempty"` // Monitor only active when a transaction is ongoing on a component relevant to this transaction. + Value float64 `json:"value"` // Value for threshold or delta monitoring. For Periodic or PeriodicClockAligned this is the interval in seconds. + Type MonitorType `json:"type" validate:"required,monitorType"` // The type of this monitor, e.g. a threshold, delta or periodic monitor. + Severity int `json:"severity" validate:"min=0,max=9"` // The severity that will be assigned to an event that is triggered by this monitor. The severity range is 0-9, with 0 as the highest and 9 as the lowest severity level. + Component types.Component `json:"component" validate:"required"` // Component for which monitor is set. + Variable types.Variable `json:"variable" validate:"required"` // Variable for which monitor is set. + PeriodicEventStream *PeriodicEventStreamParams `json:"periodicEventStream,omitempty" validate:"omitempty,dive"` } type PeriodicEventStreamParams struct { - Interval *int `json:"interval,omitempty" validate:"omitempty,gte=0"` // Interval in seconds for periodic monitoring. - ClockAligned *int `json:"clockAligned,omitempty" validate:"omitempty,gte=0"` + Interval *int `json:"interval,omitempty" validate:"omitempty,gte=0"` // Interval in seconds for periodic monitoring. + Values *int `json:"Values,omitempty" validate:"omitempty,gte=0"` } // Holds the result of SetVariableMonitoring request. From ab2382b53f19590a878ffa41e2c5c62b71844b82 Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Mon, 23 Jun 2025 22:24:41 +0200 Subject: [PATCH 05/12] feat: add tariffs and costs --- ocpp2.1/transactions/transaction_event.go | 6 +- ocpp2.1/types/cost.go | 85 +++++++++++++++++++++++ ocpp2.1/types/types.go | 2 + 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/ocpp2.1/transactions/transaction_event.go b/ocpp2.1/transactions/transaction_event.go index 99320b90..a8ad5715 100644 --- a/ocpp2.1/transactions/transaction_event.go +++ b/ocpp2.1/transactions/transaction_event.go @@ -36,9 +36,9 @@ type TransactionEventRequest struct { EvseSleep *bool `json:"evseSleep,omitempty"` TransactionInfo Transaction `json:"transactionInfo" validate:"required"` // Contains transaction specific information. 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. - // todo CostDetails types.Cos + 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. + CostDetails *types.CostDetails `json:"costDetails,omitempty" validate:"omitempty,dive"` // Contains the cost details for this transaction. This can be used to inform the CSMS about the cost of this transaction. } // This field definition of the TransactionEventResponse payload, sent by the CSMS to the Charging Station in response to a TransactionEventRequest. diff --git a/ocpp2.1/types/cost.go b/ocpp2.1/types/cost.go index 1a09b3e4..a88b3957 100644 --- a/ocpp2.1/types/cost.go +++ b/ocpp2.1/types/cost.go @@ -1,6 +1,7 @@ package types import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" "gopkg.in/go-playground/validator.v9" ) @@ -74,3 +75,87 @@ func NewSalesTariff(id int, salesTariffEntries []SalesTariffEntry) *SalesTariff SalesTariffEntry: salesTariffEntries, } } + +type TariffCost string + +const ( + TariffCostNormal = "NormalCost" + TariffCostMinimum = "MinCost" + TariffCostMaximum = "MaxCost" +) + +func isValidTariffCost(fl validator.FieldLevel) bool { + costType := TariffCost(fl.Field().String()) + switch costType { + case TariffCostNormal, TariffCostMinimum, TariffCostMaximum: + return true + default: + return false + } +} + +type CostDimensionType string + +const ( + CostDimensionTypeEnergy CostDimensionType = "Energy" // Cost dimension for energy consumed. + CostDimensionTypeChargingTime CostDimensionType = "ChargingTime" // Cost dimension for the time the EV was charging. + CostDimensionTypeIdleTime CostDimensionType = "IdleTime" // Cost dimension for the time the EV was idle. + CostDimensionMinCurrent CostDimensionType = "MinCurrent" // Cost dimension for the minimum current used during the charging session. + CostDimensionMaxCurrent CostDimensionType = "MaxCurrent" // Cost dimension for the maximum current used during the charging session. + CostDimensionMinPower CostDimensionType = "MinPower" // Cost dimension for the minimum power used during the charging session. + CostDimensionMaxPower CostDimensionType = "MaxPower" // Cost dimension for the maximum power used during the charging session. +) + +func isValidCostDimensionType(fl validator.FieldLevel) bool { + dimensionType := CostDimensionType(fl.Field().String()) + switch dimensionType { + case CostDimensionTypeEnergy, CostDimensionTypeChargingTime, CostDimensionTypeIdleTime, + CostDimensionMinCurrent, CostDimensionMaxCurrent, CostDimensionMinPower, CostDimensionMaxPower: + return true + default: + return false + } +} + +type CostDetails struct { + FailureToCalculate *bool `json:"failureToCalculate,omitempty"` + FailureReason *string `json:"failureReason,omitempty" validate:"omitempty,max=500"` + ChargingPeriods []ChargingPeriod `json:"chargingPeriods,omitempty" validate:"omitempty,dive"` + TotalCost TotalCost `json:"totalCost" validate:"required,dive"` + TotalUsage TotalUsage `json:"totalUsage" validate:"required,dive"` // Total usage of the EV during the charging session. +} + +type TotalCost struct { + Currency string `json:"currency" validate:"required,max=3"` // The currency of the total cost. + TypeOfCost TariffCost `json:"typeOfCost" validate:"required,tariffCost21"` // The type of cost, e.g. NormalCost, MinCost, MaxCost. + Fixed *Price `json:"fixedPrice,omitempty" validate:"omitempty,dive"` + Energy *Price `json:"energy,omitempty" validate:"omitempty,dive"` + ChargingTime *Price `json:"chargingTime,omitempty" validate:"omitempty,dive"` + IdleTime *Price `json:"idleTime,omitempty" validate:"omitempty,dive"` + ReservationTime *Price `json:"reservationTime,omitempty" validate:"omitempty,dive"` + Total TotalPrice `json:"total" validate:"required,dive"` // Total cost of the charging session. + ReservationFixed *Price `json:"reservationFixed,omitempty" validate:"omitempty,dive"` +} + +type TotalPrice struct { + ExclTax *float64 `json:"exclTax,omitempty" validate:"omitempty"` // Total price excluding tax. + InclTax *float64 `json:"inclTax,omitempty" validate:"omitempty"` // Total price including tax. +} + +type TotalUsage struct { + Energy float64 `json:"energy" validate:"required"` + ChargingTime int `json:"chargingTime" validate:"required"` + IdleTime int `json:"idleTime" validate:"required"` // Total idle time of the EV during the charging session, in seconds. + ReservationTime *int `json:"reservationTime,omitempty" validate:"omitempty"` // Total reservation time of the EV during the charging session, in seconds. +} + +type ChargingPeriod struct { + TariffId *string `json:"tariffId,omitempty" validate:"omitempty,max=60"` // The ID of the tariff used for this charging period. + StartPeriod types.DateTime `json:"startPeriod" validate:"required"` // The start of the charging period. + Dimensions []CostDimension `json:"dimensions,omitempty" validate:"omitempty,dive"` +} + +type CostDimension struct { + Type CostDimensionType `json:"type" validate:"required,costDimension21"` + Volume float64 `json:"volume" validate:"required"` +} diff --git a/ocpp2.1/types/types.go b/ocpp2.1/types/types.go index 36f6742e..d0636080 100644 --- a/ocpp2.1/types/types.go +++ b/ocpp2.1/types/types.go @@ -193,6 +193,8 @@ func init() { _ = Validate.RegisterValidation("15118EVCertificate21", isValidCertificate15118EVStatus) _ = Validate.RegisterValidation("costKind21", isValidCostKind) _ = Validate.RegisterValidation("operationMode21", isValidOperationMode) + _ = Validate.RegisterValidation("tariffCost21", isValidTariffCost) + _ = Validate.RegisterValidation("costDimension21", isValidCostDimensionType) Validate.RegisterStructValidation(isValidIdToken, IdToken{}) Validate.RegisterStructValidation(isValidGroupIdToken, GroupIdToken{}) From 07b70450be6072b2cd901d49ba2e8f274f68892e Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Mon, 23 Jun 2025 22:24:53 +0200 Subject: [PATCH 06/12] fix: DER profile name --- ocpp2.1/der/der.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocpp2.1/der/der.go b/ocpp2.1/der/der.go index b650dbbe..9fa1b637 100644 --- a/ocpp2.1/der/der.go +++ b/ocpp2.1/der/der.go @@ -18,7 +18,7 @@ type ChargingStationHandler interface { OnClearDERControl(chargingStationId string, req *ClearDERControlRequest) (res *ClearDERControlResponse, err error) } -const ProfileName = "DER" +const ProfileName = "DERControl" var Profile = ocpp.NewProfile( ProfileName, From 6dc755a6c656692fea0f2f7068f0ed23b109e8a6 Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Wed, 25 Jun 2025 00:03:01 +0200 Subject: [PATCH 07/12] feat: added ocpp 2.1 to docker test --- docker-compose.test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml index 7c220bbd..549bf771 100644 --- a/docker-compose.test.yaml +++ b/docker-compose.test.yaml @@ -19,8 +19,10 @@ services: go test -v -covermode=count -coverprofile=coverage.out ./ocppj go test -v -covermode=count -coverprofile=ocpp16.out -coverpkg=github.com/lorenzodonini/ocpp-go/ocpp1.6/... github.com/lorenzodonini/ocpp-go/ocpp1.6_test go test -v -covermode=count -coverprofile=ocpp201.out -coverpkg=github.com/lorenzodonini/ocpp-go/ocpp2.0.1/... github.com/lorenzodonini/ocpp-go/ocpp2.0.1_test + go test -v -covermode=count -coverprofile=ocpp21.out -coverpkg=github.com/lorenzodonini/ocpp-go/ocpp2.1/... github.com/lorenzodonini/ocpp-go/ocpp2.1_test sed '1d;$d' ocpp16.out >> coverage.out sed '1d;$d' ocpp201.out >> coverage.out + sed '1d;$d' ocpp21.out >> coverage.out integration_test: image: cimg/go:1.22.5 From 9951a0ab5d8c04e2a1eb9a8dcb59947d8ee566b2 Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Thu, 26 Jun 2025 00:04:09 +0200 Subject: [PATCH 08/12] feat: added support for send and call result err events --- ocppj/client.go | 36 +++++++++++++++++++++ ocppj/ocppj.go | 79 +++++++++++++++++++++++++++++++++++++++++++-- ocppj/ocppj_test.go | 51 +++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 2 deletions(-) diff --git a/ocppj/client.go b/ocppj/client.go index ff2fc18c..720575f7 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -3,6 +3,7 @@ package ocppj import ( "errors" "fmt" + "strings" "gopkg.in/go-playground/validator.v9" @@ -208,6 +209,35 @@ func (c *Client) SendRequest(request ocpp.Request) error { return nil } +func (c *Client) SendEvent(request ocpp.Request) error { + if !c.dispatcher.IsRunning() { + return fmt.Errorf("ocppj client is not started, couldn't send request") + } + + // Check if the request feature is a Stream feature + feature := request.GetFeatureName() + if !strings.HasSuffix(feature, "Stream") { + return fmt.Errorf("ocppj client can only send events for Stream features, got: %s", feature) + } + + call, err := c.CreateCall(request) + if err != nil { + return err + } + jsonMessage, err := call.MarshalJSON() + if err != nil { + return err + } + // Message will be processed by dispatcher. A dedicated mechanism allows to delegate the message queue handling. + if err = c.dispatcher.SendRequest(RequestBundle{Call: call, Data: jsonMessage}); err != nil { + log.Errorf("error dispatching request [%s, %s]: %v", call.UniqueId, call.Action, err) + return err + } + + log.Debugf("enqueued CALL [%s, %s]", call.UniqueId, call.Action) + return nil +} + // Sends an OCPP Response to the server. // The requestID parameter is required and identifies the previously received request. // @@ -313,6 +343,12 @@ func (c *Client) ocppMessageHandler(data []byte) error { if c.errorHandler != nil { c.errorHandler(ocpp.NewError(callError.ErrorCode, callError.ErrorDescription, callError.UniqueId), callError.ErrorDetails) } + case CALL_RESULT_ERROR: + callError := message.(*CallResultError) + log.Debugf("handling incoming CALL RESULT ERROR [%s]", callError.UniqueId) + if c.errorHandler != nil { + c.errorHandler(ocpp.NewError(callError.ErrorCode, callError.ErrorDescription, callError.UniqueId), callError.ErrorDetails) + } } } return nil diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index 63a4a9d5..7da7a494 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -182,7 +182,41 @@ func (callError *CallError) MarshalJSON() ([]byte, error) { return ocppMessageToJson(fields) } -// -------------------- Call -------------------- +// -------------------- Call Result Error -------------------- + +// An OCPP-J CallResultError message, containing an OCPP Result Error. +type CallResultError struct { + Message + 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:"omitempty,max=255"` + ErrorDetails interface{} `json:"errorDetails" validate:"omitempty"` +} + +func (callError *CallResultError) GetMessageTypeId() MessageType { + return callError.MessageTypeId +} + +func (callError *CallResultError) GetUniqueId() string { + return callError.UniqueId +} + +func (callError *CallResultError) MarshalJSON() ([]byte, error) { + fields := make([]interface{}, 5) + fields[0] = int(callError.MessageTypeId) + fields[1] = callError.UniqueId + fields[2] = callError.ErrorCode + fields[3] = callError.ErrorDescription + if callError.ErrorDetails == nil { + fields[4] = struct{}{} + } else { + fields[4] = callError.ErrorDetails + } + return ocppMessageToJson(fields) +} + +// -------------------- Send -------------------- // An OCPP-J SEND message, containing an OCPP Request. type Send struct { @@ -259,7 +293,7 @@ func IsErrorCodeValid(fl validator.FieldLevel) bool { case NotImplemented, NotSupported, InternalError, MessageTypeNotSupported, ProtocolError, SecurityError, FormatViolationV16, FormatViolationV2, PropertyConstraintViolation, OccurrenceConstraintViolationV16, OccurrenceConstraintViolationV2, - TypeConstraintViolation, GenericError: + TypeConstraintViolation, GenericError, RpcFrameworkError: return true } return false @@ -526,6 +560,47 @@ func (endpoint *Endpoint) CreateCall(request ocpp.Request) (*Call, error) { return &call, nil } +func (endpoint *Endpoint) CreateSend(request ocpp.Request) (*Send, error) { + action := request.GetFeatureName() + profile, _ := endpoint.GetProfileForFeature(action) + if profile == nil { + return nil, fmt.Errorf("Couldn't create Send for unsupported action %v", action) + } + // TODO: handle collisions? + uniqueId := messageIdGenerator() + call := Send{ + MessageTypeId: SEND, + UniqueId: uniqueId, + Action: action, + Payload: request, + } + if validationEnabled { + err := Validate.Struct(call) + if err != nil { + return nil, err + } + } + return &call, nil +} + +// Creates a CallResultError message, given the message's unique ID and the error. +func (endpoint *Endpoint) CreateCallResultError(uniqueId string, code ocpp.ErrorCode, description string, details interface{}) (*CallResultError, error) { + callError := CallResultError{ + MessageTypeId: CALL_RESULT_ERROR, + UniqueId: uniqueId, + ErrorCode: code, + ErrorDescription: description, + ErrorDetails: details, + } + if validationEnabled { + err := Validate.Struct(callError) + if err != nil { + return nil, err + } + } + return &callError, nil +} + // Creates a CallResult message, given an OCPP response and the message's unique ID. // // Returns an error in case the response's feature is not supported on this endpoint. diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index cae20e56..f6d16268 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -292,6 +292,15 @@ func CheckCall(call *ocppj.Call, t *testing.T, expectedAction string, expectedId assert.Nil(t, err) } +func CheckSend(call *ocppj.Send, t *testing.T, expectedAction string, expectedId string) { + assert.Equal(t, ocppj.SEND, call.GetMessageTypeId()) + assert.Equal(t, expectedAction, call.Action) + assert.Equal(t, expectedId, call.GetUniqueId()) + assert.NotNil(t, call.Payload) + err := Validate.Struct(call) + assert.Nil(t, err) +} + func ParseCallResult(endpoint *ocppj.Endpoint, state ocppj.ClientState, json string, t *testing.T) *ocppj.CallResult { parsedData, err := ocppj.ParseJsonMessage(json) require.NoError(t, err) @@ -336,6 +345,16 @@ func CheckCallError(t *testing.T, callError *ocppj.CallError, expectedId string, assert.Nil(t, err) } +func CheckCallResultError(t *testing.T, callError *ocppj.CallResultError, expectedId string, expectedError ocpp.ErrorCode, expectedDescription string, expectedDetails interface{}) { + assert.Equal(t, ocppj.CALL_RESULT_ERROR, callError.GetMessageTypeId()) + assert.Equal(t, expectedId, callError.GetUniqueId()) + assert.Equal(t, expectedError, callError.ErrorCode) + assert.Equal(t, expectedDescription, callError.ErrorDescription) + assert.Equal(t, expectedDetails, callError.ErrorDetails) + err := Validate.Struct(callError) + assert.Nil(t, err) +} + func assertPanic(t *testing.T, f func(), recoveredAssertion func(interface{})) { defer func() { r := recover() @@ -496,6 +515,23 @@ func (suite *OcppJTestSuite) TestCreateCall() { assert.Nil(t, pendingRequest) } +func (suite *OcppJTestSuite) TestCreateSend() { + t := suite.T() + mockValue := "somevalue" + request := newMockRequest(mockValue) + call, err := suite.chargePoint.CreateSend(request) + assert.Nil(t, err) + CheckSend(call, t, MockFeatureName+"Stream", call.UniqueId) + message, ok := call.Payload.(*MockRequest) + assert.True(t, ok) + assert.NotNil(t, message) + assert.Equal(t, mockValue, message.MockValue) + // Check that request was not yet stored as pending request + pendingRequest, exists := suite.chargePoint.RequestState.GetPendingRequest(call.UniqueId) + assert.False(t, exists) + assert.Nil(t, pendingRequest) +} + func (suite *OcppJTestSuite) TestCreateCallResult() { t := suite.T() mockValue := "someothervalue" @@ -525,6 +561,21 @@ func (suite *OcppJTestSuite) TestCreateCallError() { CheckCallError(t, callError, mockUniqueId, ocppj.GenericError, mockDescription, mockDetails) } +func (suite *OcppJTestSuite) TestCreateCallResultError() { + t := suite.T() + mockUniqueId := "123456" + mockDescription := "somedescription" + mockDetailString := "somedetailstring" + type MockDetails struct { + DetailString string + } + mockDetails := MockDetails{DetailString: mockDetailString} + callError, err := suite.chargePoint.CreateCallResultError(mockUniqueId, ocppj.GenericError, mockDescription, mockDetails) + assert.Nil(t, err) + assert.NotNil(t, callError) + CheckCallResultError(t, callError, mockUniqueId, ocppj.GenericError, mockDescription, mockDetails) +} + func (suite *OcppJTestSuite) TestParseMessageInvalidLength() { t := suite.T() mockMessage := make([]interface{}, 2) From b20a4e0f7172522a908a1db448182cf70d5f97d8 Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Thu, 26 Jun 2025 00:04:47 +0200 Subject: [PATCH 09/12] feat: added support for new messages in CSMS and CS --- .../battery_swap.go | 2 +- .../{battey_swap => battery_swap}/profile.go | 4 +- .../request_battery_swap.go | 2 +- .../{battey_swap => battery_swap}/types.go | 2 +- ocpp2.1/charging_station.go | 120 +++++- ocpp2.1/csms.go | 389 ++++++++++++++---- ocpp2.1/der/der.go | 6 +- .../adjust_periodic_event_stream.go | 2 +- ocpp2.1/tariffcost/clear_tariffs.go | 6 +- ocpp2.1/tariffcost/tariff_cost.go | 9 + ocpp2.1/v21.go | 81 +++- ocpp2.1/v2x/v2x.go | 4 +- 12 files changed, 529 insertions(+), 98 deletions(-) rename ocpp2.1/{battey_swap => battery_swap}/battery_swap.go (99%) rename ocpp2.1/{battey_swap => battery_swap}/profile.go (79%) rename ocpp2.1/{battey_swap => battery_swap}/request_battery_swap.go (98%) rename ocpp2.1/{battey_swap => battery_swap}/types.go (97%) diff --git a/ocpp2.1/battey_swap/battery_swap.go b/ocpp2.1/battery_swap/battery_swap.go similarity index 99% rename from ocpp2.1/battey_swap/battery_swap.go rename to ocpp2.1/battery_swap/battery_swap.go index ee4152be..45581aa6 100644 --- a/ocpp2.1/battey_swap/battery_swap.go +++ b/ocpp2.1/battery_swap/battery_swap.go @@ -1,4 +1,4 @@ -package battey_swap +package battery_swap import ( "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" diff --git a/ocpp2.1/battey_swap/profile.go b/ocpp2.1/battery_swap/profile.go similarity index 79% rename from ocpp2.1/battey_swap/profile.go rename to ocpp2.1/battery_swap/profile.go index e91b2ce7..fa3c2672 100644 --- a/ocpp2.1/battey_swap/profile.go +++ b/ocpp2.1/battery_swap/profile.go @@ -1,4 +1,4 @@ -package battey_swap +package battery_swap import ( "github.com/lorenzodonini/ocpp-go/ocpp" @@ -11,7 +11,7 @@ type CSMSHandler interface { // Needs to be implemented by Charging stations for handling messages part of the Battery Swap. type ChargingStationHandler interface { - OnRequestBatterySwap(chargingStationID string, request *RequestBatterySwapRequest) (*RequestBatterySwapResponse, error) + OnRequestBatterySwap(request *RequestBatterySwapRequest) (*RequestBatterySwapResponse, error) } const ProfileName = "BatterySwap" diff --git a/ocpp2.1/battey_swap/request_battery_swap.go b/ocpp2.1/battery_swap/request_battery_swap.go similarity index 98% rename from ocpp2.1/battey_swap/request_battery_swap.go rename to ocpp2.1/battery_swap/request_battery_swap.go index d6baf7f6..241d9d7f 100644 --- a/ocpp2.1/battey_swap/request_battery_swap.go +++ b/ocpp2.1/battery_swap/request_battery_swap.go @@ -1,4 +1,4 @@ -package battey_swap +package battery_swap import ( "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" diff --git a/ocpp2.1/battey_swap/types.go b/ocpp2.1/battery_swap/types.go similarity index 97% rename from ocpp2.1/battey_swap/types.go rename to ocpp2.1/battery_swap/types.go index f782728f..91643ed3 100644 --- a/ocpp2.1/battey_swap/types.go +++ b/ocpp2.1/battery_swap/types.go @@ -1,4 +1,4 @@ -package battey_swap +package battery_swap import ( "github.com/lorenzodonini/ocpp-go/ocppj" diff --git a/ocpp2.1/charging_station.go b/ocpp2.1/charging_station.go index d6af3d32..a43e15af 100644 --- a/ocpp2.1/charging_station.go +++ b/ocpp2.1/charging_station.go @@ -2,6 +2,9 @@ package ocpp21 import ( "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/battery_swap" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/der" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/v2x" "reflect" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" @@ -44,6 +47,9 @@ type chargingStation struct { diagnosticsHandler diagnostics.ChargingStationHandler displayHandler display.ChargingStationHandler dataHandler data.ChargingStationHandler + derHandler der.ChargingStationHandler + batterySwapHandler battery_swap.ChargingStationHandler + v2xHandler v2x.ChargingStationHandler responseHandler chan ocpp.Response errorHandler chan error callbacks callbackqueue.CallbackQueue @@ -396,6 +402,41 @@ func (cs *chargingStation) TransactionEvent(t transactions.TransactionEvent, tim } } +func (cs *chargingStation) BatterySwap(request battery_swap.BatterySwapRequest, props ...func(request *battery_swap.BatterySwapRequest)) (*battery_swap.BatterySwapResponse, error) { + //TODO implement me + panic("implement me") +} + +func (cs *chargingStation) NotifyDERAlarm(request der.NotifyDERAlarmRequest, props ...func(request *der.NotifyDERAlarmRequest)) (*der.NotifyDERAlarmResponse, error) { + //TODO implement me + panic("implement me") +} + +func (cs *chargingStation) NotifyDERStartStop(request der.NotifyDERStartStopRequest, props ...func(request *der.NotifyDERStartStopRequest)) (*der.NotifyDERStartStopResponse, error) { + //TODO implement me + panic("implement me") +} + +func (cs *chargingStation) ReportDERControl(request der.ReportDERControlRequest, props ...func(request *der.ReportDERControlRequest)) (*der.ReportDERControlResponse, error) { + //TODO implement me + panic("implement me") +} + +func (cs *chargingStation) ClosePeriodicEventStream(id int, props ...func(request *diagnostics.ClosePeriodicEventStreamRequest)) (*diagnostics.ClosePeriodicEventStreamResponse, error) { + //TODO implement me + panic("implement me") +} + +func (cs *chargingStation) OpenPeriodicEventStream(periodicEventStream diagnostics.PeriodicEventStreamParams, props ...func(request *diagnostics.OpenPeriodicEventStreamRequest)) (*diagnostics.OpenPeriodicEventStreamResponse, error) { + //TODO implement me + panic("implement me") +} + +func (cs *chargingStation) NotifyPeriodicEventStream(periodicEventStream diagnostics.NotifyPeriodicEventStream, props ...func(request *diagnostics.NotifyPeriodicEventStream)) { + //TODO implement me + panic("implement me") +} + func (cs *chargingStation) SetSecurityHandler(handler security.ChargingStationHandler) { cs.securityHandler = handler } @@ -460,6 +501,18 @@ func (cs *chargingStation) SetDataHandler(handler data.ChargingStationHandler) { cs.dataHandler = handler } +func (cs *chargingStation) SetDerHandler(handler der.ChargingStationHandler) { + cs.derHandler = handler +} + +func (cs *chargingStation) SetBatterySwapHandler(handler battery_swap.ChargingStationHandler) { + cs.batterySwapHandler = handler +} + +func (cs *chargingStation) SetV2XHandler(handler v2x.ChargingStationHandler) { + cs.v2xHandler = handler +} + func (cs *chargingStation) SendRequest(request ocpp.Request) (ocpp.Response, error) { featureName := request.GetFeatureName() if _, found := cs.client.GetProfileForFeature(featureName); !found { @@ -489,6 +542,27 @@ func (cs *chargingStation) SendRequest(request ocpp.Request) (ocpp.Response, err return asyncResult.r, asyncResult.e } +func (cs *chargingStation) SendEvent(request ocpp.Request) error { + featureName := request.GetFeatureName() + if _, found := cs.client.GetProfileForFeature(featureName); !found { + return fmt.Errorf("feature %v is unsupported on charging station (missing profile), cannot send event", featureName) + } + + if featureName != diagnostics.NotifyPeriodicEventStreamFeat { + return fmt.Errorf("feature %v is not valid, cannot send event", featureName) + } + + send := func() error { + return cs.client.SendEvent(request) + } + err := cs.callbacks.TryQueue("main", send, func(confirmation ocpp.Response, err error) {}) + if err != nil { + return fmt.Errorf("unable to queue event %v: %w", featureName, err) + } + + return nil +} + func (cs *chargingStation) SendRequestAsync(request ocpp.Request, callback func(response ocpp.Response, err error)) error { featureName := request.GetFeatureName() if _, found := cs.client.GetProfileForFeature(featureName); !found { @@ -512,6 +586,9 @@ func (cs *chargingStation) SendRequestAsync(request ocpp.Request, callback func( smartcharging.NotifyEVChargingScheduleFeatureName, diagnostics.NotifyEventFeatureName, diagnostics.NotifyMonitoringReportFeatureName, + diagnostics.OpenPeriodicEventStream, + diagnostics.ClosePeriodicEventStream, + diagnostics.NotifyPeriodicEventStreamFeat, provisioning.NotifyReportFeatureName, firmware.PublishFirmwareStatusNotificationFeatureName, smartcharging.ReportChargingProfilesFeatureName, @@ -519,11 +596,15 @@ func (cs *chargingStation) SendRequestAsync(request ocpp.Request, callback func( security.SecurityEventNotificationFeatureName, security.SignCertificateFeatureName, availability.StatusNotificationFeatureName, - transactions.TransactionEventFeatureName: + transactions.TransactionEventFeatureName, + battery_swap.BatterySwap, + der.NotifyDERAlarm, + der.NotifyDERStartStop: break default: return fmt.Errorf("unsupported action %v on charging station, cannot send request", featureName) } + // Response will be retrieved asynchronously via asyncHandler send := func() error { return cs.client.SendRequest(request) @@ -706,6 +787,18 @@ func (cs *chargingStation) handleIncomingRequest(request ocpp.Request, requestId if cs.transactionsHandler == nil { supported = false } + case der.ProfileName: + if cs.derHandler == nil { + supported = false + } + case battery_swap.ProfileName: + if cs.batterySwapHandler == nil { + supported = false + } + case v2x.ProfileName: + if cs.v2xHandler == nil { + supported = false + } } if !supported { cs.notSupportedError(requestId, action) @@ -796,6 +889,31 @@ func (cs *chargingStation) handleIncomingRequest(request ocpp.Request, requestId response, err = cs.firmwareHandler.OnUnpublishFirmware(request.(*firmware.UnpublishFirmwareRequest)) case firmware.UpdateFirmwareFeatureName: response, err = cs.firmwareHandler.OnUpdateFirmware(request.(*firmware.UpdateFirmwareRequest)) + case diagnostics.AdjustPeriodicEventStream: + response, err = cs.diagnosticsHandler.OnAdjustPeriodicEventStream(request.(*diagnostics.AdjustPeriodicEventStreamRequest)) + case diagnostics.GetPeriodicEventStream: + response, err = cs.diagnosticsHandler.OnGetPeriodicEventStream(request.(*diagnostics.GetPeriodicEventStreamRequest)) + case der.ClearDERControl: + response, err = cs.derHandler.OnClearDERControl(request.(*der.ClearDERControlRequest)) + case der.GetDERControl: + response, err = cs.derHandler.OnGetDERControl(request.(*der.GetDERControlRequest)) + case der.SetDERControl: + response, err = cs.derHandler.OnSetDERControl(request.(*der.SetDERControlRequest)) + case battery_swap.RequestBatterySwap: + response, err = cs.batterySwapHandler.OnRequestBatterySwap(request.(*battery_swap.RequestBatterySwapRequest)) + case tariffcost.ChangeTransactionTariff: + response, err = cs.tariffCostHandler.OnChangeTransactionTariff(request.(*tariffcost.ChangeTransactionTariffRequest)) + case tariffcost.GetTariffsFeatureName: + response, err = cs.tariffCostHandler.OnGetTariffs(request.(*tariffcost.GetTariffsRequest)) + case tariffcost.SetDefaultTariffFeatureName: + response, err = cs.tariffCostHandler.OnSetDefaultTariff(request.(*tariffcost.SetDefaultTariffRequest)) + case tariffcost.ClearTariffs: + response, err = cs.tariffCostHandler.OnClearTariffs(request.(*tariffcost.ClearTariffsRequest)) + case v2x.AFRRSignal: + response, err = cs.v2xHandler.OnAFRRSignal(request.(*v2x.AFRRSignalRequest)) + case v2x.NotifyAllowedEnergyTransfer: + response, err = cs.v2xHandler.OnNotifyAllowedEnergyTransfer(request.(*v2x.NotifyAllowedEnergyTransferRequest)) + default: cs.notSupportedError(requestId, action) return diff --git a/ocpp2.1/csms.go b/ocpp2.1/csms.go index 914f4a6f..e6269383 100644 --- a/ocpp2.1/csms.go +++ b/ocpp2.1/csms.go @@ -2,13 +2,13 @@ package ocpp21 import ( "fmt" - "reflect" - "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ocpp2.1/authorization" "github.com/lorenzodonini/ocpp-go/ocpp2.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/battery_swap" "github.com/lorenzodonini/ocpp-go/ocpp2.1/data" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/der" "github.com/lorenzodonini/ocpp-go/ocpp2.1/diagnostics" "github.com/lorenzodonini/ocpp-go/ocpp2.1/display" "github.com/lorenzodonini/ocpp-go/ocpp2.1/firmware" @@ -23,8 +23,10 @@ import ( "github.com/lorenzodonini/ocpp-go/ocpp2.1/tariffcost" "github.com/lorenzodonini/ocpp-go/ocpp2.1/transactions" "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/v2x" "github.com/lorenzodonini/ocpp-go/ocppj" "github.com/lorenzodonini/ocpp-go/ws" + "reflect" ) type csms struct { @@ -45,6 +47,9 @@ type csms struct { diagnosticsHandler diagnostics.CSMSHandler displayHandler display.CSMSHandler dataHandler data.CSMSHandler + batterySwapHandler battery_swap.CSMSHandler + derControlHandler der.CSMSHandler + v2xHandler v2x.CSMSHandler callbackQueue callbackqueue.CallbackQueue errC chan error } @@ -673,6 +678,185 @@ func (cs *csms) UpdateFirmware(clientId string, callback func(*firmware.UpdateFi return cs.SendRequestAsync(clientId, request, genericCallback) } +func (cs *csms) ChangeTransactionTariff(clientId string, callback func(*tariffcost.ChangeTransactionTariffResponse, error), transactionId string, tariff types.Tariff, props ...func(request *tariffcost.ChangeTransactionTariffRequest)) error { + request := tariffcost.NewChangeTransactionTariffRequest(transactionId, tariff) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*tariffcost.ChangeTransactionTariffResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetTariffs(clientId string, callback func(*tariffcost.GetTariffsResponse, error), evseId int, props ...func(request *tariffcost.GetTariffsRequest)) error { + request := tariffcost.NewGetTariffsRequest(evseId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*tariffcost.GetTariffsResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetDefaultTariff(clientId string, callback func(*tariffcost.SetDefaultTariffResponse, error), evseId int, tariff types.Tariff, props ...func(request *tariffcost.SetDefaultTariffRequest)) error { + request := tariffcost.NewSetDefaultTariffRequest(evseId, tariff) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*tariffcost.SetDefaultTariffResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ClearTariffs(clientId string, callback func(*tariffcost.ClearTariffsResponse, error), props ...func(request *tariffcost.ClearTariffsRequest)) error { + request := tariffcost.NewClearTariffsRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*tariffcost.ClearTariffsResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) AdjustPeriodicEventStream(clientId string, callback func(*diagnostics.AdjustPeriodicEventStreamResponse, error), id int, periodicEventStream diagnostics.PeriodicEventStreamParams, props ...func(request *diagnostics.AdjustPeriodicEventStreamRequest)) error { + request := diagnostics.NewAdjustPeriodicEventStreamsRequest(id, periodicEventStream) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.AdjustPeriodicEventStreamResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetPeriodicEventStream(clientId string, callback func(*diagnostics.GetPeriodicEventStreamResponse, error), props ...func(request *diagnostics.GetPeriodicEventStreamRequest)) error { + request := diagnostics.NewGetPeriodicEventStreamsRequest() + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*diagnostics.GetPeriodicEventStreamResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) RequestBatterySwap(clientId string, callback func(*battery_swap.RequestBatterySwapResponse, error), request battery_swap.RequestBatterySwapRequest, props ...func(request *battery_swap.RequestBatterySwapRequest)) error { + for _, fn := range props { + fn(&request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*battery_swap.RequestBatterySwapResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) ClearDERControl(clientId string, callback func(*der.ClearDERControlResponse, error), isDefault bool, props ...func(request *der.ClearDERControlRequest)) error { + request := der.NewClearDERControlRequest(isDefault) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*der.ClearDERControlResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) GetDERControl(clientId string, callback func(*der.GetDERControlResponse, error), requestId int, props ...func(request *der.GetDERControlRequest)) error { + request := der.NewGetDERControlResponseRequest(requestId) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*der.GetDERControlResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) SetDERControl(clientId string, callback func(*der.SetDERControlResponse, error), isDefault bool, controlId string, derControl der.DERControl, props ...func(request *der.SetDERControlRequest)) error { + request := der.NewSetDERControlResponseRequest(isDefault, controlId, derControl) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*der.SetDERControlResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) AFRRSignal(clientId string, callback func(*v2x.AFRRSignalResponse, error), timestamp types.DateTime, signal int, props ...func(request *v2x.AFRRSignalRequest)) error { + request := v2x.NewAFRRSignalRequest(×tamp, signal) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*v2x.AFRRSignalResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + +func (cs *csms) NotifyAllowedEnergyTransfer(clientId string, callback func(*v2x.NotifyAllowedEnergyTransferResponse, error), transactionId string, allowedModes []types.EnergyTransferMode, props ...func(request *v2x.NotifyAllowedEnergyTransferRequest)) error { + request := v2x.NewNotifyAllowedEnergyTransferRequest(transactionId, allowedModes...) + for _, fn := range props { + fn(request) + } + genericCallback := func(response ocpp.Response, protoError error) { + if response != nil { + callback(response.(*v2x.NotifyAllowedEnergyTransferResponse), protoError) + } else { + callback(nil, protoError) + } + } + return cs.SendRequestAsync(clientId, request, genericCallback) +} + func (cs *csms) SetSecurityHandler(handler security.CSMSHandler) { cs.securityHandler = handler } @@ -737,6 +921,18 @@ func (cs *csms) SetDataHandler(handler data.CSMSHandler) { cs.dataHandler = handler } +func (cs *csms) SetBatterySwapHandler(handler battery_swap.CSMSHandler) { + cs.batterySwapHandler = handler +} + +func (cs *csms) SetDERControlHandler(handler der.CSMSHandler) { + cs.derControlHandler = handler +} + +func (cs *csms) SetV2XHandler(handler v2x.CSMSHandler) { + cs.v2xHandler = handler +} + func (cs *csms) SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) { cs.server.SetNewClientValidationHandler(handler) } @@ -802,7 +998,19 @@ func (cs *csms) SendRequestAsync(clientId string, request ocpp.Request, callback remotecontrol.TriggerMessageFeatureName, remotecontrol.UnlockConnectorFeatureName, firmware.UnpublishFirmwareFeatureName, - firmware.UpdateFirmwareFeatureName: + firmware.UpdateFirmwareFeatureName, + der.GetDERControl, + der.SetDERControl, + der.ClearDERControl, + v2x.AFRRSignal, + v2x.NotifyAllowedEnergyTransfer, + battery_swap.RequestBatterySwap, + diagnostics.AdjustPeriodicEventStream, + diagnostics.GetPeriodicEventStream, + tariffcost.ClearTariffs, + tariffcost.GetTariffsFeatureName, + tariffcost.SetDefaultTariffFeatureName, + tariffcost.ChangeTransactionTariff: break default: return fmt.Errorf("unsupported action %v on CSMS, cannot send request", featureName) @@ -877,86 +1085,101 @@ func (cs *csms) notSupportedError(chargingStationID string, requestId string, ac } func (cs *csms) handleIncomingRequest(chargingStation ChargingStationConnection, request ocpp.Request, requestId string, action string) { - profile, found := cs.server.GetProfileForFeature(action) // Check whether action is supported and a listener for it exists + profile, found := cs.server.GetProfileForFeature(action) if !found { cs.notImplementedError(chargingStation.ID(), requestId, action) return - } else { - supported := true - switch profile.Name { - case authorization.ProfileName: - if cs.authorizationHandler == nil { - supported = false - } - case availability.ProfileName: - if cs.availabilityHandler == nil { - supported = false - } - case data.ProfileName: - if cs.dataHandler == nil { - supported = false - } - case diagnostics.ProfileName: - if cs.diagnosticsHandler == nil { - supported = false - } - case display.ProfileName: - if cs.displayHandler == nil { - supported = false - } - case firmware.ProfileName: - if cs.firmwareHandler == nil { - supported = false - } - case iso15118.ProfileName: - if cs.iso15118Handler == nil { - supported = false - } - case localauth.ProfileName: - if cs.localAuthListHandler == nil { - supported = false - } - case meter.ProfileName: - if cs.meterHandler == nil { - supported = false - } - case provisioning.ProfileName: - if cs.provisioningHandler == nil { - supported = false - } - case remotecontrol.ProfileName: - if cs.remoteControlHandler == nil { - supported = false - } - case reservation.ProfileName: - if cs.reservationHandler == nil { - supported = false - } - case security.ProfileName: - if cs.securityHandler == nil { - supported = false - } - case smartcharging.ProfileName: - if cs.smartChargingHandler == nil { - supported = false - } - case tariffcost.ProfileName: - if cs.tariffCostHandler == nil { - supported = false - } - case transactions.ProfileName: - if cs.transactionsHandler == nil { - supported = false - } - } - if !supported { - cs.notSupportedError(chargingStation.ID(), requestId, action) - return + } + + supported := true + switch profile.Name { + case authorization.ProfileName: + if cs.authorizationHandler == nil { + supported = false + } + case availability.ProfileName: + if cs.availabilityHandler == nil { + supported = false + } + case data.ProfileName: + if cs.dataHandler == nil { + supported = false + } + case diagnostics.ProfileName: + if cs.diagnosticsHandler == nil { + supported = false + } + case display.ProfileName: + if cs.displayHandler == nil { + supported = false + } + case firmware.ProfileName: + if cs.firmwareHandler == nil { + supported = false + } + case iso15118.ProfileName: + if cs.iso15118Handler == nil { + supported = false + } + case localauth.ProfileName: + if cs.localAuthListHandler == nil { + supported = false + } + case meter.ProfileName: + if cs.meterHandler == nil { + supported = false + } + case provisioning.ProfileName: + if cs.provisioningHandler == nil { + supported = false + } + case remotecontrol.ProfileName: + if cs.remoteControlHandler == nil { + supported = false + } + case reservation.ProfileName: + if cs.reservationHandler == nil { + supported = false + } + case security.ProfileName: + if cs.securityHandler == nil { + supported = false + } + case smartcharging.ProfileName: + if cs.smartChargingHandler == nil { + supported = false + } + case tariffcost.ProfileName: + if cs.tariffCostHandler == nil { + supported = false + } + case transactions.ProfileName: + if cs.transactionsHandler == nil { + supported = false + } + case v2x.ProfileName: + if cs.v2xHandler == nil { + supported = false + } + case battery_swap.ProfileName: + if cs.batterySwapHandler == nil { + supported = false + } + case der.ProfileName: + if cs.derControlHandler == nil { + supported = false } } + + if !supported { + cs.notSupportedError(chargingStation.ID(), requestId, action) + return + } + var response ocpp.Response var err error + // Execute in separate goroutine, so the caller goroutine is available go func() { switch action { @@ -1010,6 +1233,22 @@ func (cs *csms) handleIncomingRequest(chargingStation ChargingStationConnection, response, err = cs.availabilityHandler.OnStatusNotification(chargingStation.ID(), request.(*availability.StatusNotificationRequest)) case transactions.TransactionEventFeatureName: response, err = cs.transactionsHandler.OnTransactionEvent(chargingStation.ID(), request.(*transactions.TransactionEventRequest)) + case battery_swap.BatterySwap: + response, err = cs.batterySwapHandler.OnBatterySwap(chargingStation.ID(), request.(*battery_swap.BatterySwapRequest)) + case der.NotifyDERAlarm: + response, err = cs.derControlHandler.OnNotifyDERAlarm(chargingStation.ID(), request.(*der.NotifyDERAlarmRequest)) + case der.NotifyDERStartStop: + response, err = cs.derControlHandler.OnNotifyDERStartStop(chargingStation.ID(), request.(*der.NotifyDERStartStopRequest)) + case der.ReportDERControl: + response, err = cs.derControlHandler.OnReportDERControl(chargingStation.ID(), request.(*der.ReportDERControlRequest)) + case diagnostics.OpenPeriodicEventStream: + response, err = cs.diagnosticsHandler.OnOpenPeriodicEventStream(chargingStation.ID(), request.(*diagnostics.OpenPeriodicEventStreamRequest)) + case diagnostics.ClosePeriodicEventStream: + response, err = cs.diagnosticsHandler.OnClosePeriodicEventStream(chargingStation.ID(), request.(*diagnostics.ClosePeriodicEventStreamRequest)) + case diagnostics.NotifyPeriodicEventStreamFeat: + // No response needed or expected + cs.diagnosticsHandler.OnNotifyPeriodicEventStream(chargingStation.ID(), request.(*diagnostics.NotifyPeriodicEventStream)) + return default: cs.notSupportedError(chargingStation.ID(), requestId, action) return diff --git a/ocpp2.1/der/der.go b/ocpp2.1/der/der.go index 9fa1b637..75d99883 100644 --- a/ocpp2.1/der/der.go +++ b/ocpp2.1/der/der.go @@ -13,9 +13,9 @@ type CSMSHandler interface { // Needs to be implemented by Charging stations for handling messages part of the OCPP 2.1 DER profile. type ChargingStationHandler interface { - OnGetDERControl(chargingStationId string, req *GetDERControlRequest) (res *GetDERControlResponse, err error) - OnSetDERControl(chargingStationId string, req *SetDERControlRequest) (res *SetDERControlResponse, err error) - OnClearDERControl(chargingStationId string, req *ClearDERControlRequest) (res *ClearDERControlResponse, err error) + OnGetDERControl(req *GetDERControlRequest) (res *GetDERControlResponse, err error) + OnSetDERControl(req *SetDERControlRequest) (res *SetDERControlResponse, err error) + OnClearDERControl(req *ClearDERControlRequest) (res *ClearDERControlResponse, err error) } const ProfileName = "DERControl" diff --git a/ocpp2.1/diagnostics/adjust_periodic_event_stream.go b/ocpp2.1/diagnostics/adjust_periodic_event_stream.go index 9c348af4..b732d0b7 100644 --- a/ocpp2.1/diagnostics/adjust_periodic_event_stream.go +++ b/ocpp2.1/diagnostics/adjust_periodic_event_stream.go @@ -7,7 +7,7 @@ import ( // -------------------- Adjust Periodic EventStream (CSMS -> CS) -------------------- -const AdjustPeriodicEventStream = "ClosePeriodicEventStream" +const AdjustPeriodicEventStream = "AdjustPeriodicEventStream" // The field definition of the AdjustPeriodicEventStreamRequest request payload sent by the CSMS to the Charging Station. type AdjustPeriodicEventStreamRequest struct { diff --git a/ocpp2.1/tariffcost/clear_tariffs.go b/ocpp2.1/tariffcost/clear_tariffs.go index 68b16277..6b8e18a4 100644 --- a/ocpp2.1/tariffcost/clear_tariffs.go +++ b/ocpp2.1/tariffcost/clear_tariffs.go @@ -67,10 +67,8 @@ func (c ClearTariffsResponse) GetFeatureName() string { } // Creates a new NewClearTariffsRequest, containing all required fields. There are no optional fields for this message. -func NewClearTariffsRequest(tariffIds []string) *ClearTariffsRequest { - return &ClearTariffsRequest{ - TariffIds: tariffIds, - } +func NewClearTariffsRequest() *ClearTariffsRequest { + return &ClearTariffsRequest{} } // Creates a new NewClearTariffsResponse, which doesn't contain any required or optional fields. diff --git a/ocpp2.1/tariffcost/tariff_cost.go b/ocpp2.1/tariffcost/tariff_cost.go index bcfc075c..1c1895ac 100644 --- a/ocpp2.1/tariffcost/tariff_cost.go +++ b/ocpp2.1/tariffcost/tariff_cost.go @@ -11,6 +11,14 @@ type CSMSHandler interface { type ChargingStationHandler interface { // OnCostUpdated is called on a charging station whenever a CostUpdatedRequest is received from the CSMS. OnCostUpdated(request *CostUpdatedRequest) (confirmation *CostUpdatedResponse, err error) + // OnSetDefaultTariff is called on a charging station whenever a SetDefaultTariffRequest is received from the CSMS. + OnSetDefaultTariff(request *SetDefaultTariffRequest) (confirmation *SetDefaultTariffResponse, err error) + // OnGetTariffs is called on a charging station whenever a GetTariffsRequest is received from the CSMS. + OnGetTariffs(request *GetTariffsRequest) (confirmation *GetTariffsResponse, err error) + // OnClearTariffs is called on a charging station whenever a ClearTariffsResponse is received from the CSMS. + OnClearTariffs(request *ClearTariffsRequest) (confirmation *ClearTariffsResponse, err error) + // OnChangeTransactionTariff is called on a charging station whenever a ChangeTransactionTariffRequest is received from the CSMS. + OnChangeTransactionTariff(request *ChangeTransactionTariffRequest) (confirmation *ChangeTransactionTariffResponse, err error) } const ProfileName = "TariffCost" @@ -21,4 +29,5 @@ var Profile = ocpp.NewProfile( SetDefaultTariffFeature{}, GetTariffsFeature{}, ClearTariffsFeature{}, + ChangeTransactionTariffFeature{}, ) diff --git a/ocpp2.1/v21.go b/ocpp2.1/v21.go index 35ca72aa..152a0add 100644 --- a/ocpp2.1/v21.go +++ b/ocpp2.1/v21.go @@ -3,13 +3,13 @@ package ocpp21 import ( "crypto/tls" - "net" - "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ocpp2.1/authorization" "github.com/lorenzodonini/ocpp-go/ocpp2.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/battery_swap" "github.com/lorenzodonini/ocpp-go/ocpp2.1/data" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/der" "github.com/lorenzodonini/ocpp-go/ocpp2.1/diagnostics" "github.com/lorenzodonini/ocpp-go/ocpp2.1/display" "github.com/lorenzodonini/ocpp-go/ocpp2.1/firmware" @@ -24,8 +24,10 @@ import ( "github.com/lorenzodonini/ocpp-go/ocpp2.1/tariffcost" "github.com/lorenzodonini/ocpp-go/ocpp2.1/transactions" "github.com/lorenzodonini/ocpp-go/ocpp2.1/types" + "github.com/lorenzodonini/ocpp-go/ocpp2.1/v2x" "github.com/lorenzodonini/ocpp-go/ocppj" "github.com/lorenzodonini/ocpp-go/ws" + "net" ) type ChargingStationConnection interface { @@ -113,6 +115,15 @@ type ChargingStation interface { StatusNotification(timestamp *types.DateTime, status availability.ConnectorStatus, evseID int, connectorID int, props ...func(request *availability.StatusNotificationRequest)) (*availability.StatusNotificationResponse, error) // Sends information to the CSMS about a transaction, used for billing purposes. TransactionEvent(t transactions.TransactionEvent, timestamp *types.DateTime, reason transactions.TriggerReason, seqNo int, info transactions.Transaction, props ...func(request *transactions.TransactionEventRequest)) (*transactions.TransactionEventResponse, error) + + BatterySwap(request battery_swap.BatterySwapRequest, props ...func(request *battery_swap.BatterySwapRequest)) (*battery_swap.BatterySwapResponse, error) + NotifyDERAlarm(request der.NotifyDERAlarmRequest, props ...func(request *der.NotifyDERAlarmRequest)) (*der.NotifyDERAlarmResponse, error) + NotifyDERStartStop(request der.NotifyDERStartStopRequest, props ...func(request *der.NotifyDERStartStopRequest)) (*der.NotifyDERStartStopResponse, error) + ReportDERControl(request der.ReportDERControlRequest, props ...func(request *der.ReportDERControlRequest)) (*der.ReportDERControlResponse, error) + ClosePeriodicEventStream(id int, props ...func(request *diagnostics.ClosePeriodicEventStreamRequest)) (*diagnostics.ClosePeriodicEventStreamResponse, error) + OpenPeriodicEventStream(periodicEventStream diagnostics.PeriodicEventStreamParams, props ...func(request *diagnostics.OpenPeriodicEventStreamRequest)) (*diagnostics.OpenPeriodicEventStreamResponse, error) + NotifyPeriodicEventStream(periodicEventStream diagnostics.NotifyPeriodicEventStream, props ...func(request *diagnostics.NotifyPeriodicEventStream)) + // Registers a handler for incoming security profile messages SetSecurityHandler(handler security.ChargingStationHandler) // Registers a handler for incoming provisioning profile messages @@ -145,6 +156,13 @@ type ChargingStation interface { SetDisplayHandler(handler display.ChargingStationHandler) // Registers a handler for incoming data transfer messages SetDataHandler(handler data.ChargingStationHandler) + // Registers a handler for incoming DER control messages + SetDerHandler(handler der.ChargingStationHandler) + // Registers a handler for incoming battery swap messages + SetBatterySwapHandler(handler battery_swap.ChargingStationHandler) + // Registers a handler for incoming V2X messages + SetV2XHandler(handler v2x.ChargingStationHandler) + // Sends a request to the CSMS. // The CSMS will respond with a confirmation, or with an error if the request was invalid or could not be processed. // In case of network issues (i.e. the remote host couldn't be reached), the function also returns an error. @@ -183,7 +201,7 @@ type ChargingStation interface { Errors() <-chan error } -// Creates a new OCPP 2.0 charging station client. +// Creates a new OCPP 2.1 charging station client. // The id parameter is required to uniquely identify the charge point. // // The endpoint and client parameters may be omitted, in order to use a default configuration: @@ -216,7 +234,31 @@ func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) Cha if endpoint == nil { dispatcher := ocppj.NewDefaultClientDispatcher(ocppj.NewFIFOClientQueue(0)) - endpoint = ocppj.NewClient(id, client, dispatcher, nil, authorization.Profile, availability.Profile, data.Profile, diagnostics.Profile, display.Profile, firmware.Profile, iso15118.Profile, localauth.Profile, meter.Profile, provisioning.Profile, remotecontrol.Profile, reservation.Profile, security.Profile, smartcharging.Profile, tariffcost.Profile, transactions.Profile) + endpoint = ocppj.NewClient( + id, + client, + dispatcher, + nil, + authorization.Profile, + availability.Profile, + data.Profile, + diagnostics.Profile, + display.Profile, + firmware.Profile, + iso15118.Profile, + localauth.Profile, + meter.Profile, + provisioning.Profile, + remotecontrol.Profile, + reservation.Profile, + security.Profile, + smartcharging.Profile, + tariffcost.Profile, + transactions.Profile, + v2x.Profile, + battery_swap.Profile, + der.Profile, + ) } endpoint.SetDialect(ocpp.V2) @@ -240,7 +282,7 @@ func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) Cha return &cs } -// -------------------- v2.0 CSMS -------------------- +// -------------------- v2.1 CSMS -------------------- // A Charging Station Management System (CSMS) manages Charging Stations and has the information for authorizing Management Users for using its Charging Stations. // You can instantiate a default CSMS struct by calling the NewCSMS function. @@ -349,6 +391,23 @@ type CSMS interface { // Instructs a Charging Station to download and install a firmware update. UpdateFirmware(clientId string, callback func(*firmware.UpdateFirmwareResponse, error), requestID int, firmware firmware.Firmware, props ...func(request *firmware.UpdateFirmwareRequest)) error + ChangeTransactionTariff(clientId string, callback func(*tariffcost.ChangeTransactionTariffResponse, error), transactionId string, tariff types.Tariff, props ...func(request *tariffcost.ChangeTransactionTariffRequest)) error + + GetTariffs(clientId string, callback func(*tariffcost.GetTariffsResponse, error), evseId int, props ...func(request *tariffcost.GetTariffsRequest)) error + SetDefaultTariff(clientId string, callback func(*tariffcost.SetDefaultTariffResponse, error), evseId int, tariff types.Tariff, props ...func(request *tariffcost.SetDefaultTariffRequest)) error + ClearTariffs(clientId string, callback func(*tariffcost.ClearTariffsResponse, error), props ...func(request *tariffcost.ClearTariffsRequest)) error + + AdjustPeriodicEventStream(clientId string, callback func(*diagnostics.AdjustPeriodicEventStreamResponse, error), id int, periodicEventStream diagnostics.PeriodicEventStreamParams, props ...func(request *diagnostics.AdjustPeriodicEventStreamRequest)) error + GetPeriodicEventStream(clientId string, callback func(*diagnostics.GetPeriodicEventStreamResponse, error), props ...func(request *diagnostics.GetPeriodicEventStreamRequest)) error + + RequestBatterySwap(clientId string, callback func(*battery_swap.RequestBatterySwapResponse, error), request battery_swap.RequestBatterySwapRequest, props ...func(request *battery_swap.RequestBatterySwapRequest)) error + ClearDERControl(clientId string, callback func(*der.ClearDERControlResponse, error), isDefault bool, props ...func(request *der.ClearDERControlRequest)) error + GetDERControl(clientId string, callback func(*der.GetDERControlResponse, error), requestId int, props ...func(request *der.GetDERControlRequest)) error + SetDERControl(clientId string, callback func(*der.SetDERControlResponse, error), isDefault bool, controlId string, derControl der.DERControl, props ...func(request *der.SetDERControlRequest)) error + + AFRRSignal(clientId string, callback func(*v2x.AFRRSignalResponse, error), timestamp types.DateTime, signal int, props ...func(request *v2x.AFRRSignalRequest)) error + NotifyAllowedEnergyTransfer(clientId string, callback func(*v2x.NotifyAllowedEnergyTransferResponse, error), transactionId string, allowedModes []types.EnergyTransferMode, props ...func(request *v2x.NotifyAllowedEnergyTransferRequest)) error + // Registers a handler for incoming security profile messages. SetSecurityHandler(handler security.CSMSHandler) // Registers a handler for incoming provisioning profile messages. @@ -381,6 +440,11 @@ type CSMS interface { SetDisplayHandler(handler display.CSMSHandler) // Registers a handler for incoming data transfer messages SetDataHandler(handler data.CSMSHandler) + // Registers a handler for incoming DER control messages + SetDERControlHandler(handler der.CSMSHandler) + // Registers a handler for incoming battery swap messages + SetBatterySwapHandler(handler battery_swap.CSMSHandler) + // Registers a handler for new incoming Charging station connections. SetNewChargingStationValidationHandler(handler ws.CheckClientHandler) // Registers a handler for new incoming Charging station connections. @@ -403,14 +467,14 @@ type CSMS interface { Errors() <-chan error } -// Creates a new OCPP 2.0 CSMS. +// Creates a new OCPP 2.1 CSMS. // // The endpoint and client parameters may be omitted, in order to use a default configuration: // // csms := NewCSMS(nil, nil) // // It is recommended to use the default configuration, unless a custom networking / ocppj layer is required. -// The default dispatcher supports all implemented OCPP 2.0 features out-of-the-box. +// The default dispatcher supports all implemented OCPP 2.1 features out-of-the-box. // // If you need a TLS server, you may use the following: // @@ -438,6 +502,9 @@ func NewCSMS(endpoint *ocppj.Server, server ws.Server) CSMS { smartcharging.Profile, tariffcost.Profile, transactions.Profile, + v2x.Profile, + battery_swap.Profile, + der.Profile, ) } cs := newCSMS(endpoint) diff --git a/ocpp2.1/v2x/v2x.go b/ocpp2.1/v2x/v2x.go index 4eb2182e..c8f870e7 100644 --- a/ocpp2.1/v2x/v2x.go +++ b/ocpp2.1/v2x/v2x.go @@ -10,8 +10,8 @@ type CSMSHandler interface { // Needs to be implemented by Charging stations for handling messages part of the V2X. type ChargingStationHandler interface { - OnAFRRSignal(chargingStationId string, request *AFRRSignalRequest) (*AFRRSignalResponse, error) - OnNotifyAllowedEnergyTransfer(chargingStationId string, request *NotifyAllowedEnergyTransferRequest) (*NotifyAllowedEnergyTransferResponse, error) + OnAFRRSignal(request *AFRRSignalRequest) (*AFRRSignalResponse, error) + OnNotifyAllowedEnergyTransfer(request *NotifyAllowedEnergyTransferRequest) (*NotifyAllowedEnergyTransferResponse, error) } const ProfileName = "V2X" From 3f5ee9247efdb2d1d19a82d5cdb2bf66649d24db Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Thu, 26 Jun 2025 18:10:14 +0200 Subject: [PATCH 10/12] fix: missing charge point request implementations, invalid dialect --- ocpp2.1/charging_station.go | 180 +++++++++++------- ocpp2.1/csms.go | 9 +- .../close_periodic_event_stream.go | 2 +- ocpp2.1/v21.go | 26 ++- 4 files changed, 140 insertions(+), 77 deletions(-) diff --git a/ocpp2.1/charging_station.go b/ocpp2.1/charging_station.go index a43e15af..edb37026 100644 --- a/ocpp2.1/charging_station.go +++ b/ocpp2.1/charging_station.go @@ -85,9 +85,9 @@ func (cs *chargingStation) BootNotification(reason provisioning.BootReason, mode response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*provisioning.BootNotificationResponse), err } + + return response.(*provisioning.BootNotificationResponse), err } func (cs *chargingStation) Authorize(idToken string, tokenType types.IdTokenType, props ...func(request *authorization.AuthorizeRequest)) (*authorization.AuthorizeResponse, error) { @@ -98,9 +98,9 @@ func (cs *chargingStation) Authorize(idToken string, tokenType types.IdTokenType response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*authorization.AuthorizeResponse), err } + + return response.(*authorization.AuthorizeResponse), err } func (cs *chargingStation) ClearedChargingLimit(chargingLimitSource types.ChargingLimitSourceType, props ...func(request *smartcharging.ClearedChargingLimitRequest)) (*smartcharging.ClearedChargingLimitResponse, error) { @@ -111,9 +111,9 @@ func (cs *chargingStation) ClearedChargingLimit(chargingLimitSource types.Chargi response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*smartcharging.ClearedChargingLimitResponse), err } + + return response.(*smartcharging.ClearedChargingLimitResponse), err } func (cs *chargingStation) DataTransfer(vendorId string, props ...func(request *data.DataTransferRequest)) (*data.DataTransferResponse, error) { @@ -124,9 +124,9 @@ func (cs *chargingStation) DataTransfer(vendorId string, props ...func(request * response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*data.DataTransferResponse), err } + + return response.(*data.DataTransferResponse), err } func (cs *chargingStation) FirmwareStatusNotification(status firmware.FirmwareStatus, props ...func(request *firmware.FirmwareStatusNotificationRequest)) (*firmware.FirmwareStatusNotificationResponse, error) { @@ -137,9 +137,9 @@ func (cs *chargingStation) FirmwareStatusNotification(status firmware.FirmwareSt response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*firmware.FirmwareStatusNotificationResponse), err } + + return response.(*firmware.FirmwareStatusNotificationResponse), err } func (cs *chargingStation) Get15118EVCertificate(schemaVersion string, action iso15118.CertificateAction, exiRequest string, props ...func(request *iso15118.Get15118EVCertificateRequest)) (*iso15118.Get15118EVCertificateResponse, error) { @@ -150,9 +150,9 @@ func (cs *chargingStation) Get15118EVCertificate(schemaVersion string, action is response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*iso15118.Get15118EVCertificateResponse), err } + + return response.(*iso15118.Get15118EVCertificateResponse), err } func (cs *chargingStation) GetCertificateStatus(ocspRequestData types.OCSPRequestDataType, props ...func(request *iso15118.GetCertificateStatusRequest)) (*iso15118.GetCertificateStatusResponse, error) { @@ -163,9 +163,9 @@ func (cs *chargingStation) GetCertificateStatus(ocspRequestData types.OCSPReques response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*iso15118.GetCertificateStatusResponse), err } + + return response.(*iso15118.GetCertificateStatusResponse), err } func (cs *chargingStation) Heartbeat(props ...func(request *availability.HeartbeatRequest)) (*availability.HeartbeatResponse, error) { @@ -176,9 +176,9 @@ func (cs *chargingStation) Heartbeat(props ...func(request *availability.Heartbe response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*availability.HeartbeatResponse), err } + + return response.(*availability.HeartbeatResponse), err } func (cs *chargingStation) LogStatusNotification(status diagnostics.UploadLogStatus, requestID int, props ...func(request *diagnostics.LogStatusNotificationRequest)) (*diagnostics.LogStatusNotificationResponse, error) { @@ -189,9 +189,9 @@ func (cs *chargingStation) LogStatusNotification(status diagnostics.UploadLogSta response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*diagnostics.LogStatusNotificationResponse), err } + + return response.(*diagnostics.LogStatusNotificationResponse), err } func (cs *chargingStation) MeterValues(evseID int, meterValues []types.MeterValue, props ...func(request *meter.MeterValuesRequest)) (*meter.MeterValuesResponse, error) { @@ -202,9 +202,9 @@ func (cs *chargingStation) MeterValues(evseID int, meterValues []types.MeterValu response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*meter.MeterValuesResponse), err } + + return response.(*meter.MeterValuesResponse), err } func (cs *chargingStation) NotifyChargingLimit(chargingLimit smartcharging.ChargingLimit, props ...func(request *smartcharging.NotifyChargingLimitRequest)) (*smartcharging.NotifyChargingLimitResponse, error) { @@ -215,9 +215,9 @@ func (cs *chargingStation) NotifyChargingLimit(chargingLimit smartcharging.Charg response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*smartcharging.NotifyChargingLimitResponse), err } + + return response.(*smartcharging.NotifyChargingLimitResponse), err } func (cs *chargingStation) NotifyCustomerInformation(data string, seqNo int, generatedAt types.DateTime, requestID int, props ...func(request *diagnostics.NotifyCustomerInformationRequest)) (*diagnostics.NotifyCustomerInformationResponse, error) { @@ -228,9 +228,9 @@ func (cs *chargingStation) NotifyCustomerInformation(data string, seqNo int, gen response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*diagnostics.NotifyCustomerInformationResponse), err } + + return response.(*diagnostics.NotifyCustomerInformationResponse), err } func (cs *chargingStation) NotifyDisplayMessages(requestID int, props ...func(request *display.NotifyDisplayMessagesRequest)) (*display.NotifyDisplayMessagesResponse, error) { @@ -241,9 +241,9 @@ func (cs *chargingStation) NotifyDisplayMessages(requestID int, props ...func(re response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*display.NotifyDisplayMessagesResponse), err } + + return response.(*display.NotifyDisplayMessagesResponse), err } func (cs *chargingStation) NotifyEVChargingNeeds(evseID int, chargingNeeds smartcharging.ChargingNeeds, props ...func(request *smartcharging.NotifyEVChargingNeedsRequest)) (*smartcharging.NotifyEVChargingNeedsResponse, error) { @@ -254,9 +254,9 @@ func (cs *chargingStation) NotifyEVChargingNeeds(evseID int, chargingNeeds smart response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*smartcharging.NotifyEVChargingNeedsResponse), err } + + return response.(*smartcharging.NotifyEVChargingNeedsResponse), err } func (cs *chargingStation) NotifyEVChargingSchedule(timeBase *types.DateTime, evseID int, schedule types.ChargingSchedule, props ...func(request *smartcharging.NotifyEVChargingScheduleRequest)) (*smartcharging.NotifyEVChargingScheduleResponse, error) { @@ -267,9 +267,9 @@ func (cs *chargingStation) NotifyEVChargingSchedule(timeBase *types.DateTime, ev response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*smartcharging.NotifyEVChargingScheduleResponse), err } + + return response.(*smartcharging.NotifyEVChargingScheduleResponse), err } func (cs *chargingStation) NotifyEvent(generatedAt *types.DateTime, seqNo int, eventData []diagnostics.EventData, props ...func(request *diagnostics.NotifyEventRequest)) (*diagnostics.NotifyEventResponse, error) { @@ -280,9 +280,9 @@ func (cs *chargingStation) NotifyEvent(generatedAt *types.DateTime, seqNo int, e response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*diagnostics.NotifyEventResponse), err } + + return response.(*diagnostics.NotifyEventResponse), err } func (cs *chargingStation) NotifyMonitoringReport(requestID int, seqNo int, generatedAt *types.DateTime, monitorData []diagnostics.MonitoringData, props ...func(request *diagnostics.NotifyMonitoringReportRequest)) (*diagnostics.NotifyMonitoringReportResponse, error) { @@ -293,9 +293,9 @@ func (cs *chargingStation) NotifyMonitoringReport(requestID int, seqNo int, gene response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*diagnostics.NotifyMonitoringReportResponse), err } + + return response.(*diagnostics.NotifyMonitoringReportResponse), err } func (cs *chargingStation) NotifyReport(requestID int, generatedAt *types.DateTime, seqNo int, props ...func(request *provisioning.NotifyReportRequest)) (*provisioning.NotifyReportResponse, error) { @@ -306,9 +306,9 @@ func (cs *chargingStation) NotifyReport(requestID int, generatedAt *types.DateTi response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*provisioning.NotifyReportResponse), err } + + return response.(*provisioning.NotifyReportResponse), err } func (cs *chargingStation) PublishFirmwareStatusNotification(status firmware.PublishFirmwareStatus, props ...func(request *firmware.PublishFirmwareStatusNotificationRequest)) (*firmware.PublishFirmwareStatusNotificationResponse, error) { @@ -319,9 +319,9 @@ func (cs *chargingStation) PublishFirmwareStatusNotification(status firmware.Pub response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*firmware.PublishFirmwareStatusNotificationResponse), err } + + return response.(*firmware.PublishFirmwareStatusNotificationResponse), err } func (cs *chargingStation) ReportChargingProfiles(requestID int, chargingLimitSource types.ChargingLimitSourceType, evseID int, chargingProfile []types.ChargingProfile, props ...func(request *smartcharging.ReportChargingProfilesRequest)) (*smartcharging.ReportChargingProfilesResponse, error) { @@ -332,9 +332,9 @@ func (cs *chargingStation) ReportChargingProfiles(requestID int, chargingLimitSo response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*smartcharging.ReportChargingProfilesResponse), err } + + return response.(*smartcharging.ReportChargingProfilesResponse), err } func (cs *chargingStation) ReservationStatusUpdate(reservationID int, status reservation.ReservationUpdateStatus, props ...func(request *reservation.ReservationStatusUpdateRequest)) (*reservation.ReservationStatusUpdateResponse, error) { @@ -345,9 +345,9 @@ func (cs *chargingStation) ReservationStatusUpdate(reservationID int, status res response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*reservation.ReservationStatusUpdateResponse), err } + + return response.(*reservation.ReservationStatusUpdateResponse), err } func (cs *chargingStation) SecurityEventNotification(typ string, timestamp *types.DateTime, props ...func(request *security.SecurityEventNotificationRequest)) (*security.SecurityEventNotificationResponse, error) { @@ -358,9 +358,8 @@ func (cs *chargingStation) SecurityEventNotification(typ string, timestamp *type response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*security.SecurityEventNotificationResponse), err } + return response.(*security.SecurityEventNotificationResponse), err } func (cs *chargingStation) SignCertificate(csr string, props ...func(request *security.SignCertificateRequest)) (*security.SignCertificateResponse, error) { @@ -371,9 +370,9 @@ func (cs *chargingStation) SignCertificate(csr string, props ...func(request *se response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*security.SignCertificateResponse), err } + + return response.(*security.SignCertificateResponse), err } func (cs *chargingStation) StatusNotification(timestamp *types.DateTime, status availability.ConnectorStatus, evseID int, connectorID int, props ...func(request *availability.StatusNotificationRequest)) (*availability.StatusNotificationResponse, error) { @@ -381,12 +380,13 @@ func (cs *chargingStation) StatusNotification(timestamp *types.DateTime, status for _, fn := range props { fn(request) } + response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*availability.StatusNotificationResponse), err } + + return response.(*availability.StatusNotificationResponse), err } func (cs *chargingStation) TransactionEvent(t transactions.TransactionEvent, timestamp *types.DateTime, reason transactions.TriggerReason, seqNo int, info transactions.Transaction, props ...func(request *transactions.TransactionEventRequest)) (*transactions.TransactionEventResponse, error) { @@ -397,44 +397,94 @@ func (cs *chargingStation) TransactionEvent(t transactions.TransactionEvent, tim response, err := cs.SendRequest(request) if err != nil { return nil, err - } else { - return response.(*transactions.TransactionEventResponse), err } + + return response.(*transactions.TransactionEventResponse), err } func (cs *chargingStation) BatterySwap(request battery_swap.BatterySwapRequest, props ...func(request *battery_swap.BatterySwapRequest)) (*battery_swap.BatterySwapResponse, error) { - //TODO implement me - panic("implement me") + for _, fn := range props { + fn(&request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } + return response.(*battery_swap.BatterySwapResponse), err } func (cs *chargingStation) NotifyDERAlarm(request der.NotifyDERAlarmRequest, props ...func(request *der.NotifyDERAlarmRequest)) (*der.NotifyDERAlarmResponse, error) { - //TODO implement me - panic("implement me") + for _, fn := range props { + fn(&request) + } + + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } + + return response.(*der.NotifyDERAlarmResponse), err } func (cs *chargingStation) NotifyDERStartStop(request der.NotifyDERStartStopRequest, props ...func(request *der.NotifyDERStartStopRequest)) (*der.NotifyDERStartStopResponse, error) { - //TODO implement me - panic("implement me") + for _, fn := range props { + fn(&request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } + + return response.(*der.NotifyDERStartStopResponse), err } func (cs *chargingStation) ReportDERControl(request der.ReportDERControlRequest, props ...func(request *der.ReportDERControlRequest)) (*der.ReportDERControlResponse, error) { - //TODO implement me - panic("implement me") + for _, fn := range props { + fn(&request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } + + return response.(*der.ReportDERControlResponse), err } func (cs *chargingStation) ClosePeriodicEventStream(id int, props ...func(request *diagnostics.ClosePeriodicEventStreamRequest)) (*diagnostics.ClosePeriodicEventStreamResponse, error) { - //TODO implement me - panic("implement me") + request := diagnostics.NewClosePeriodicEventStreamsRequest(id) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } + + return response.(*diagnostics.ClosePeriodicEventStreamResponse), err } -func (cs *chargingStation) OpenPeriodicEventStream(periodicEventStream diagnostics.PeriodicEventStreamParams, props ...func(request *diagnostics.OpenPeriodicEventStreamRequest)) (*diagnostics.OpenPeriodicEventStreamResponse, error) { - //TODO implement me - panic("implement me") +func (cs *chargingStation) OpenPeriodicEventStream(constantStreamData diagnostics.ConstantStreamData, props ...func(request *diagnostics.OpenPeriodicEventStreamRequest)) (*diagnostics.OpenPeriodicEventStreamResponse, error) { + request := diagnostics.NewOpenPeriodicEventStreamsRequest(constantStreamData) + for _, fn := range props { + fn(request) + } + response, err := cs.SendRequest(request) + if err != nil { + return nil, err + } + + return response.(*diagnostics.OpenPeriodicEventStreamResponse), err } func (cs *chargingStation) NotifyPeriodicEventStream(periodicEventStream diagnostics.NotifyPeriodicEventStream, props ...func(request *diagnostics.NotifyPeriodicEventStream)) { - //TODO implement me - panic("implement me") + for _, fn := range props { + fn(&periodicEventStream) + } + err := cs.SendEvent(periodicEventStream) + if err != nil { + // todo log + return + } } func (cs *chargingStation) SetSecurityHandler(handler security.ChargingStationHandler) { diff --git a/ocpp2.1/csms.go b/ocpp2.1/csms.go index e6269383..de3b392a 100644 --- a/ocpp2.1/csms.go +++ b/ocpp2.1/csms.go @@ -1,6 +1,7 @@ package ocpp21 import ( + "errors" "fmt" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" @@ -54,15 +55,15 @@ type csms struct { errC chan error } -func newCSMS(server *ocppj.Server) csms { +func newCSMS(server *ocppj.Server) (csms, error) { if server == nil { - panic("server must not be nil") + return csms{}, errors.New("server must not be nil") } - server.SetDialect(ocpp.V2) + server.SetDialect(ocpp.V21) return csms{ server: server, callbackQueue: callbackqueue.New(), - } + }, nil } func (cs *csms) error(err error) { diff --git a/ocpp2.1/diagnostics/close_periodic_event_stream.go b/ocpp2.1/diagnostics/close_periodic_event_stream.go index a74721de..32325a04 100644 --- a/ocpp2.1/diagnostics/close_periodic_event_stream.go +++ b/ocpp2.1/diagnostics/close_periodic_event_stream.go @@ -39,7 +39,7 @@ func (c ClosePeriodicEventStreamResponse) GetFeatureName() string { } // Creates a new ClosePeriodicEventStreamRequest, containing all required fields. There are no optional fields for this message. -func NewClosePeriodicEventStreamsRequest(id int, params PeriodicEventStreamParams) *ClosePeriodicEventStreamRequest { +func NewClosePeriodicEventStreamsRequest(id int) *ClosePeriodicEventStreamRequest { return &ClosePeriodicEventStreamRequest{ Id: id, } diff --git a/ocpp2.1/v21.go b/ocpp2.1/v21.go index 152a0add..9c267a0d 100644 --- a/ocpp2.1/v21.go +++ b/ocpp2.1/v21.go @@ -3,6 +3,7 @@ package ocpp21 import ( "crypto/tls" + "errors" "github.com/lorenzodonini/ocpp-go/internal/callbackqueue" "github.com/lorenzodonini/ocpp-go/ocpp" "github.com/lorenzodonini/ocpp-go/ocpp2.1/authorization" @@ -121,7 +122,7 @@ type ChargingStation interface { NotifyDERStartStop(request der.NotifyDERStartStopRequest, props ...func(request *der.NotifyDERStartStopRequest)) (*der.NotifyDERStartStopResponse, error) ReportDERControl(request der.ReportDERControlRequest, props ...func(request *der.ReportDERControlRequest)) (*der.ReportDERControlResponse, error) ClosePeriodicEventStream(id int, props ...func(request *diagnostics.ClosePeriodicEventStreamRequest)) (*diagnostics.ClosePeriodicEventStreamResponse, error) - OpenPeriodicEventStream(periodicEventStream diagnostics.PeriodicEventStreamParams, props ...func(request *diagnostics.OpenPeriodicEventStreamRequest)) (*diagnostics.OpenPeriodicEventStreamResponse, error) + OpenPeriodicEventStream(constantStreamData diagnostics.ConstantStreamData, props ...func(request *diagnostics.OpenPeriodicEventStreamRequest)) (*diagnostics.OpenPeriodicEventStreamResponse, error) NotifyPeriodicEventStream(periodicEventStream diagnostics.NotifyPeriodicEventStream, props ...func(request *diagnostics.NotifyPeriodicEventStream)) // Registers a handler for incoming security profile messages @@ -226,7 +227,11 @@ type ChargingStation interface { // // For more advanced options, or if a custom networking/occpj layer is required, // please refer to ocppj.Client and ws.Client. -func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) ChargingStation { +func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) (ChargingStation, error) { + if id == "" { + return nil, errors.New("id must not be empty") + } + if client == nil { client = ws.NewClient() } @@ -260,7 +265,7 @@ func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) Cha der.Profile, ) } - endpoint.SetDialect(ocpp.V2) + endpoint.SetDialect(ocpp.V21) cs := chargingStation{ client: endpoint, @@ -278,8 +283,9 @@ func NewChargingStation(id string, endpoint *ocppj.Client, client ws.Client) Cha cs.client.SetErrorHandler(func(err *ocpp.Error, details interface{}) { cs.errorHandler <- err }) + cs.client.SetRequestHandler(cs.handleIncomingRequest) - return &cs + return &cs, nil } // -------------------- v2.1 CSMS -------------------- @@ -479,7 +485,7 @@ type CSMS interface { // If you need a TLS server, you may use the following: // // csms := NewCSMS(nil, ws.NewServer(ws.WithServerTLSConfig("certificatePath", "privateKeyPath", nil))) -func NewCSMS(endpoint *ocppj.Server, server ws.Server) CSMS { +func NewCSMS(endpoint *ocppj.Server, server ws.Server) (CSMS, error) { if server == nil { server = ws.NewServer() } @@ -507,7 +513,12 @@ func NewCSMS(endpoint *ocppj.Server, server ws.Server) CSMS { der.Profile, ) } - cs := newCSMS(endpoint) + + cs, err := newCSMS(endpoint) + if err != nil { + return nil, err + } + cs.server.SetRequestHandler(func(client ws.Channel, request ocpp.Request, requestId string, action string) { cs.handleIncomingRequest(client, request, requestId, action) }) @@ -520,5 +531,6 @@ func NewCSMS(endpoint *ocppj.Server, server ws.Server) CSMS { cs.server.SetCanceledRequestHandler(func(clientID string, requestID string, request ocpp.Request, err *ocpp.Error) { cs.handleCanceledRequest(clientID, request, err) }) - return &cs + + return &cs, nil } From 178648e9a29aa6a91d1e28ef665e1f010192cece Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Wed, 2 Jul 2025 15:06:24 +0200 Subject: [PATCH 11/12] feat: updates & docs (wip) --- example/2.1/chargingstation/Dockerfile | 26 ++ .../chargingstation/authorization_handler.go | 10 + .../chargingstation/availability_handler.go | 37 +++ .../chargingstation/charging_station_sim.go | 269 ++++++++++++++++ example/2.1/chargingstation/config.go | 18 ++ example/2.1/chargingstation/data_handler.go | 24 ++ .../chargingstation/diagnostics_handler.go | 66 ++++ .../2.1/chargingstation/display_handler.go | 21 ++ .../2.1/chargingstation/firmware_handler.go | 76 +++++ example/2.1/chargingstation/handler.go | 114 +++++++ .../2.1/chargingstation/iso15118_handler.go | 22 ++ .../2.1/chargingstation/localauth_handler.go | 26 ++ .../chargingstation/provisioning_handler.go | 76 +++++ .../chargingstation/remotecontrol_handler.go | 146 +++++++++ .../chargingstation/reservation_handler.go | 132 ++++++++ .../chargingstation/smartcharging_handler.go | 27 ++ .../2.1/chargingstation/tariffcost_handler.go | 11 + .../chargingstation/transactions_handler.go | 12 + example/2.1/create-test-certificates.sh | 15 + example/2.1/csms/Dockerfile | 25 ++ example/2.1/csms/authorization_handler.go | 12 + example/2.1/csms/availability_handler.go | 30 ++ example/2.1/csms/csms_sim.go | 302 ++++++++++++++++++ example/2.1/csms/data_handler.go | 24 ++ example/2.1/csms/diagnostics_handler.go | 33 ++ example/2.1/csms/display_handler.go | 14 + example/2.1/csms/firmware_handler.go | 28 ++ example/2.1/csms/handler.go | 62 ++++ example/2.1/csms/iso15118_handler.go | 17 + example/2.1/csms/meter_handler.go | 12 + example/2.1/csms/provisioning_handler.go | 23 ++ example/2.1/csms/reservation_handler.go | 9 + example/2.1/csms/security_handler.go | 18 ++ example/2.1/csms/smartcharging_handler.go | 32 ++ example/2.1/csms/transactions_handler.go | 19 ++ example/2.1/docker-compose.tls.yml | 45 +++ example/2.1/docker-compose.yml | 31 ++ example/2.1/openssl-chargingstation.conf | 14 + example/2.1/openssl-csms.conf | 14 + ocppj/charge_point_test.go | 28 +- ocppj/client.go | 15 +- ocppj/dispatcher.go | 41 +++ ocppj/dispatcher_test.go | 32 ++ ocppj/ocppj.go | 81 ++++- ocppj/ocppj_test.go | 53 +-- ocppj/queue.go | 7 + ocppj/queue_test.go | 109 +++---- ocppj/server.go | 14 + ocppj/state_test.go | 110 +++---- 49 files changed, 2192 insertions(+), 190 deletions(-) create mode 100644 example/2.1/chargingstation/Dockerfile create mode 100644 example/2.1/chargingstation/authorization_handler.go create mode 100644 example/2.1/chargingstation/availability_handler.go create mode 100644 example/2.1/chargingstation/charging_station_sim.go create mode 100644 example/2.1/chargingstation/config.go create mode 100644 example/2.1/chargingstation/data_handler.go create mode 100644 example/2.1/chargingstation/diagnostics_handler.go create mode 100644 example/2.1/chargingstation/display_handler.go create mode 100644 example/2.1/chargingstation/firmware_handler.go create mode 100644 example/2.1/chargingstation/handler.go create mode 100644 example/2.1/chargingstation/iso15118_handler.go create mode 100644 example/2.1/chargingstation/localauth_handler.go create mode 100644 example/2.1/chargingstation/provisioning_handler.go create mode 100644 example/2.1/chargingstation/remotecontrol_handler.go create mode 100644 example/2.1/chargingstation/reservation_handler.go create mode 100644 example/2.1/chargingstation/smartcharging_handler.go create mode 100644 example/2.1/chargingstation/tariffcost_handler.go create mode 100644 example/2.1/chargingstation/transactions_handler.go create mode 100755 example/2.1/create-test-certificates.sh create mode 100644 example/2.1/csms/Dockerfile create mode 100644 example/2.1/csms/authorization_handler.go create mode 100644 example/2.1/csms/availability_handler.go create mode 100644 example/2.1/csms/csms_sim.go create mode 100644 example/2.1/csms/data_handler.go create mode 100644 example/2.1/csms/diagnostics_handler.go create mode 100644 example/2.1/csms/display_handler.go create mode 100644 example/2.1/csms/firmware_handler.go create mode 100644 example/2.1/csms/handler.go create mode 100644 example/2.1/csms/iso15118_handler.go create mode 100644 example/2.1/csms/meter_handler.go create mode 100644 example/2.1/csms/provisioning_handler.go create mode 100644 example/2.1/csms/reservation_handler.go create mode 100644 example/2.1/csms/security_handler.go create mode 100644 example/2.1/csms/smartcharging_handler.go create mode 100644 example/2.1/csms/transactions_handler.go create mode 100644 example/2.1/docker-compose.tls.yml create mode 100644 example/2.1/docker-compose.yml create mode 100644 example/2.1/openssl-chargingstation.conf create mode 100644 example/2.1/openssl-csms.conf diff --git a/example/2.1/chargingstation/Dockerfile b/example/2.1/chargingstation/Dockerfile new file mode 100644 index 00000000..6e5a5e0c --- /dev/null +++ b/example/2.1/chargingstation/Dockerfile @@ -0,0 +1,26 @@ +############################ +# STEP 1 build executable binary +############################ +FROM golang:alpine AS builder + +ENV GO111MODULE on +WORKDIR $GOPATH/src/github.com/lorenzodonini/ocpp-go +COPY . . +# Fetch dependencies. +RUN go mod download +# Build the binary. +RUN go build -ldflags="-w -s" -o /go/bin/charging_station example/2.0.1/chargingstation/*.go + +############################ +# STEP 2 build a small image +############################ +FROM alpine + +COPY --from=builder /go/bin/charging_station /bin/charging_station + +# Add CA certificates +# It currently throws a warning on alpine: WARNING: ca-certificates.crt does not contain exactly one certificate or CRL: skipping. +# Ignore the warning. +RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* && update-ca-certificates + +CMD [ "charging_station" ] diff --git a/example/2.1/chargingstation/authorization_handler.go b/example/2.1/chargingstation/authorization_handler.go new file mode 100644 index 00000000..4cffe06a --- /dev/null +++ b/example/2.1/chargingstation/authorization_handler.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/authorization" +) + +func (handler *ChargingStationHandler) OnClearCache(request *authorization.ClearCacheRequest) (response *authorization.ClearCacheResponse, err error) { + logDefault(request.GetFeatureName()).Infof("cleared mocked cache") + return authorization.NewClearCacheResponse(authorization.ClearCacheStatusAccepted), nil +} diff --git a/example/2.1/chargingstation/availability_handler.go b/example/2.1/chargingstation/availability_handler.go new file mode 100644 index 00000000..0748d398 --- /dev/null +++ b/example/2.1/chargingstation/availability_handler.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" +) + +func (handler *ChargingStationHandler) OnChangeAvailability(request *availability.ChangeAvailabilityRequest) (response *availability.ChangeAvailabilityResponse, err error) { + if request.Evse == nil { + // Changing availability for the entire charging station + handler.availability = request.OperationalStatus + // TODO: recursively update the availability for all evse/connectors + response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusAccepted) + return + } + reqEvse := request.Evse + if e, ok := handler.evse[reqEvse.ID]; ok { + // Changing availability for a specific EVSE + if reqEvse.ConnectorID != nil { + // Changing availability for a specific connector + if !e.hasConnector(*reqEvse.ConnectorID) { + response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusRejected) + } else { + connector := e.connectors[*reqEvse.ConnectorID] + connector.availability = request.OperationalStatus + e.connectors[*reqEvse.ConnectorID] = connector + response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusAccepted) + } + return + } + e.availability = request.OperationalStatus + response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusAccepted) + return + } + // No EVSE with such ID found + response = availability.NewChangeAvailabilityResponse(availability.ChangeAvailabilityStatusRejected) + return +} diff --git a/example/2.1/chargingstation/charging_station_sim.go b/example/2.1/chargingstation/charging_station_sim.go new file mode 100644 index 00000000..091723ea --- /dev/null +++ b/example/2.1/chargingstation/charging_station_sim.go @@ -0,0 +1,269 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "os" + "strconv" + "time" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/transactions" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + + "github.com/sirupsen/logrus" + + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/lorenzodonini/ocpp-go/ws" +) + +const ( + envVarClientID = "CLIENT_ID" + envVarCSMSUrl = "CSMS_URL" + envVarTls = "TLS_ENABLED" + envVarCACertificate = "CA_CERTIFICATE_PATH" + envVarClientCertificate = "CLIENT_CERTIFICATE_PATH" + envVarClientCertificateKey = "CLIENT_CERTIFICATE_KEY_PATH" +) + +var log *logrus.Logger + +func setupChargingStation(chargingStationID string) ocpp2.ChargingStation { + return ocpp2.NewChargingStation(chargingStationID, nil, nil) +} + +func setupTlsChargingStation(chargingStationID string) ocpp2.ChargingStation { + certPool, err := x509.SystemCertPool() + if err != nil { + log.Fatal(err) + } + // Load CA cert + caPath, ok := os.LookupEnv(envVarCACertificate) + if ok { + caCert, err := os.ReadFile(caPath) + if err != nil { + log.Warn(err) + } else if !certPool.AppendCertsFromPEM(caCert) { + log.Info("no ca.cert file found, will use system CA certificates") + } + } else { + log.Info("no ca.cert file found, will use system CA certificates") + } + // Load client certificate + clientCertPath, ok1 := os.LookupEnv(envVarClientCertificate) + clientKeyPath, ok2 := os.LookupEnv(envVarClientCertificateKey) + var clientCertificates []tls.Certificate + if ok1 && ok2 { + certificate, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath) + if err == nil { + clientCertificates = []tls.Certificate{certificate} + } else { + log.Infof("couldn't load client TLS certificate: %v", err) + } + } + // Create client with TLS config + client := ws.NewClient(ws.WithClientTLSConfig(&tls.Config{ + RootCAs: certPool, + Certificates: clientCertificates, + })) + return ocpp2.NewChargingStation(chargingStationID, nil, client) +} + +// exampleRoutine simulates a charging station flow, where a dummy transaction is started. +// The simulation runs for about 5 minutes. +func exampleRoutine(chargingStation ocpp2.ChargingStation, stateHandler *ChargingStationHandler) { + dummyClientIdToken := types.IdToken{ + IdToken: "12345", + Type: types.IdTokenTypeKeyCode, + } + // Boot + bootResp, err := chargingStation.BootNotification(provisioning.BootReasonPowerUp, "model1", "vendor1") + checkError(err) + logDefault(bootResp.GetFeatureName()).Infof("status: %v, interval: %v, current time: %v", bootResp.Status, bootResp.Interval, bootResp.CurrentTime.String()) + // Notify EVSE status + for eID, e := range stateHandler.evse { + updateOperationalStatus(stateHandler, eID, availability.OperationalStatusOperative) + // Notify connector status + for cID := range e.connectors { + updateConnectorStatus(stateHandler, eID, cID, availability.ConnectorStatusAvailable) + } + } + // Wait for some time ... + time.Sleep(5 * time.Second) + // Simulate charging for connector 1 + // EV is plugged in + evseID := 1 + evse := stateHandler.evse[evseID] + chargingConnector := 0 + updateConnectorStatus(stateHandler, evseID, chargingConnector, availability.ConnectorStatusOccupied) + // Start transaction + tx := transactions.Transaction{ + TransactionID: pseudoUUID(), + ChargingState: transactions.ChargingStateEVConnected, + } + evseReq := types.EVSE{ID: evseID, ConnectorID: &chargingConnector} + txEventResp, err := chargingStation.TransactionEvent(transactions.TransactionEventStarted, types.Now(), transactions.TriggerReasonCablePluggedIn, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) { + request.Evse = &evseReq + }) + checkError(err) + logDefault(txEventResp.GetFeatureName()).Infof("transaction %v started", tx.TransactionID) + stateHandler.evse[evseID].currentTransaction = tx.TransactionID + // Authorize + authResp, err := chargingStation.Authorize(dummyClientIdToken.IdToken, types.IdTokenTypeKeyCode) + checkError(err) + logDefault(authResp.GetFeatureName()).Infof("status: %v %v", authResp.IdTokenInfo.Status, getExpiryDate(&authResp.IdTokenInfo)) + // Update transaction with auth info + txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventUpdated, types.Now(), transactions.TriggerReasonAuthorized, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) { + request.Evse = &evseReq + request.IDToken = &dummyClientIdToken + }) + checkError(err) + logDefault(txEventResp.GetFeatureName()).Infof("transaction %v updated", tx.TransactionID) + // Update transaction after energy offering starts + txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventUpdated, types.Now(), transactions.TriggerReasonChargingStateChanged, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) { + request.Evse = &evseReq + request.IDToken = &dummyClientIdToken + }) + checkError(err) + logDefault(txEventResp.GetFeatureName()).Infof("transaction %v updated", tx.TransactionID) + // Periodically send meter values + var sampleInterval time.Duration = 5 + //sampleInterval, ok := stateHandler.configuration.getInt(MeterValueSampleInterval) + //if !ok { + // sampleInterval = 5 + //} + var sampledValue types.SampledValue + for i := 0; i < 5; i++ { + time.Sleep(time.Second * sampleInterval) + stateHandler.meterValue += 10 + sampledValue = types.SampledValue{ + Value: stateHandler.meterValue, + Context: types.ReadingContextSamplePeriodic, + Measurand: types.MeasurandEnergyActiveExportRegister, + Phase: types.PhaseL3, + Location: types.LocationOutlet, + UnitOfMeasure: &types.UnitOfMeasure{ + Unit: "kWh", + }, + } + meterValue := types.MeterValue{ + Timestamp: types.DateTime{Time: time.Now()}, + SampledValue: []types.SampledValue{sampledValue}, + } + // Send meter values + txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventUpdated, types.Now(), transactions.TriggerReasonMeterValuePeriodic, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) { + request.MeterValue = []types.MeterValue{meterValue} + request.IDToken = &dummyClientIdToken + }) + checkError(err) + logDefault(txEventResp.GetFeatureName()).Infof("transaction %v updated with periodic meter values", tx.TransactionID) + // Increase meter value + stateHandler.meterValue += 2 + } + // Stop charging for connector 1 + updateConnectorStatus(stateHandler, evseID, chargingConnector, availability.ConnectorStatusAvailable) + // Send transaction end data + sampledValue.Context = types.ReadingContextTransactionEnd + sampledValue.Value = stateHandler.meterValue + tx.StoppedReason = transactions.ReasonEVDisconnected + txEventResp, err = chargingStation.TransactionEvent(transactions.TransactionEventEnded, types.Now(), transactions.TriggerReasonEVCommunicationLost, evse.nextSequence(), tx, func(request *transactions.TransactionEventRequest) { + request.Evse = &evseReq + request.IDToken = &dummyClientIdToken + request.MeterValue = []types.MeterValue{} + }) + checkError(err) + logDefault(txEventResp.GetFeatureName()).Infof("transaction %v stopped", tx.TransactionID) + // Wait for some time ... + time.Sleep(5 * time.Minute) + // End simulation +} + +// Start function +func main() { + // Load config + id, ok := os.LookupEnv(envVarClientID) + if !ok { + log.Printf("no %v environment variable found, exiting...", envVarClientID) + return + } + csmsUrl, ok := os.LookupEnv(envVarCSMSUrl) + if !ok { + log.Printf("no %v environment variable found, exiting...", envVarCSMSUrl) + return + } + // Check if TLS enabled + t, _ := os.LookupEnv(envVarTls) + tlsEnabled, _ := strconv.ParseBool(t) + // Prepare OCPP 2.0.1 charging station (chargingStation variable is defined in handler.go) + if tlsEnabled { + chargingStation = setupTlsChargingStation(id) + } else { + chargingStation = setupChargingStation(id) + } + // Setup some basic state management + evse := EVSEInfo{ + availability: availability.OperationalStatusOperative, + currentTransaction: "", + currentReservation: 0, + connectors: map[int]ConnectorInfo{ + 0: { + status: availability.ConnectorStatusAvailable, + availability: availability.OperationalStatusOperative, + typ: reservation.ConnectorTypeCType2, + }, + }, + seqNo: 0, + } + handler := &ChargingStationHandler{ + model: "model1", + vendor: "vendor1", + availability: availability.OperationalStatusOperative, + evse: map[int]*EVSEInfo{1: &evse}, + localAuthList: []localauth.AuthorizationData{}, + localAuthListVersion: 0, + monitoringLevel: 0, + meterValue: 0, + } + // Support callbacks for all OCPP 2.0.1 profiles + chargingStation.SetAvailabilityHandler(handler) + chargingStation.SetAuthorizationHandler(handler) + chargingStation.SetDataHandler(handler) + chargingStation.SetDiagnosticsHandler(handler) + chargingStation.SetDisplayHandler(handler) + chargingStation.SetFirmwareHandler(handler) + chargingStation.SetISO15118Handler(handler) + chargingStation.SetLocalAuthListHandler(handler) + chargingStation.SetProvisioningHandler(handler) + chargingStation.SetRemoteControlHandler(handler) + chargingStation.SetReservationHandler(handler) + chargingStation.SetSmartChargingHandler(handler) + chargingStation.SetTariffCostHandler(handler) + chargingStation.SetTransactionsHandler(handler) + ocppj.SetLogger(log) + // Connects to central system + err := chargingStation.Start(csmsUrl) + if err != nil { + log.Error(err) + } else { + log.Infof("connected to CSMS at %v", csmsUrl) + exampleRoutine(chargingStation, handler) + // Disconnect + chargingStation.Stop() + log.Infof("disconnected from CSMS") + } +} + +func init() { + log = logrus.New() + log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) + log.SetLevel(logrus.InfoLevel) +} + +// Utility functions +func logDefault(feature string) *logrus.Entry { + return log.WithField("message", feature) +} diff --git a/example/2.1/chargingstation/config.go b/example/2.1/chargingstation/config.go new file mode 100644 index 00000000..8c866ca0 --- /dev/null +++ b/example/2.1/chargingstation/config.go @@ -0,0 +1,18 @@ +package main + +import ( + "crypto/rand" + "fmt" +) + +// Generates a pseudo UUID. Not RFC 4122 compliant, but useful for this example. +func pseudoUUID() (uuid string) { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + fmt.Println("Error: ", err) + return + } + uuid = fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + return +} diff --git a/example/2.1/chargingstation/data_handler.go b/example/2.1/chargingstation/data_handler.go new file mode 100644 index 00000000..feb4551f --- /dev/null +++ b/example/2.1/chargingstation/data_handler.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/data" +) + +type DataSample struct { + SampleString string `json:"sample_string"` + SampleValue float64 `json:"sample_value"` +} + +func (handler *ChargingStationHandler) OnDataTransfer(request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) { + var dataSample DataSample + err = json.Unmarshal(request.Data.([]byte), &dataSample) + if err != nil { + logDefault(request.GetFeatureName()). + Errorf("invalid data received: %v", request.Data) + return nil, err + } + logDefault(request.GetFeatureName()). + Infof("data received: %v, %v", dataSample.SampleString, dataSample.SampleValue) + return data.NewDataTransferResponse(data.DataTransferStatusAccepted), nil +} diff --git a/example/2.1/chargingstation/diagnostics_handler.go b/example/2.1/chargingstation/diagnostics_handler.go new file mode 100644 index 00000000..2a201120 --- /dev/null +++ b/example/2.1/chargingstation/diagnostics_handler.go @@ -0,0 +1,66 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/diagnostics" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" +) + +func (handler *ChargingStationHandler) OnClearVariableMonitoring(request *diagnostics.ClearVariableMonitoringRequest) (response *diagnostics.ClearVariableMonitoringResponse, err error) { + logDefault(request.GetFeatureName()).Infof("cleared variables %v", request.ID) + clearMonitoringResult := make([]diagnostics.ClearMonitoringResult, len(request.ID)) + for i, req := range request.ID { + res := diagnostics.ClearMonitoringResult{ + ID: req, + Status: diagnostics.ClearMonitoringStatusAccepted, + } + clearMonitoringResult[i] = res + } + return diagnostics.NewClearVariableMonitoringResponse(clearMonitoringResult), nil +} + +func (handler *ChargingStationHandler) OnCustomerInformation(request *diagnostics.CustomerInformationRequest) (response *diagnostics.CustomerInformationResponse, err error) { + logDefault(request.GetFeatureName()).Infof("request %d for customer %s (clear %v, report %v)", request.RequestID, request.CustomerIdentifier, request.Clear, request.Report) + return diagnostics.NewCustomerInformationResponse(diagnostics.CustomerInformationStatusAccepted), nil +} + +func (handler *ChargingStationHandler) OnGetLog(request *diagnostics.GetLogRequest) (response *diagnostics.GetLogResponse, err error) { + logDefault(request.GetFeatureName()).Infof("request %d to upload logs %v to %v accepted", request.RequestID, request.LogType, request.Log.RemoteLocation) + // TODO: start asynchronous log upload + return diagnostics.NewGetLogResponse(diagnostics.LogStatusAccepted), nil +} + +func (handler *ChargingStationHandler) OnGetMonitoringReport(request *diagnostics.GetMonitoringReportRequest) (response *diagnostics.GetMonitoringReportResponse, err error) { + logDefault(request.GetFeatureName()).Infof("request %d to upload report with criteria %v, component variables %v", request.RequestID, request.MonitoringCriteria, request.ComponentVariable) + // TODO: start asynchronous report upload via NotifyMonitoringReportRequest + return diagnostics.NewGetMonitoringReportResponse(types.GenericDeviceModelStatusAccepted), nil +} + +func (handler *ChargingStationHandler) OnSetMonitoringBase(request *diagnostics.SetMonitoringBaseRequest) (response *diagnostics.SetMonitoringBaseResponse, err error) { + logDefault(request.GetFeatureName()).Infof("monitoring base %s set successfully", request.MonitoringBase) + return diagnostics.NewSetMonitoringBaseResponse(types.GenericDeviceModelStatusAccepted), nil +} + +func (handler *ChargingStationHandler) OnSetMonitoringLevel(request *diagnostics.SetMonitoringLevelRequest) (response *diagnostics.SetMonitoringLevelResponse, err error) { + handler.monitoringLevel = request.Severity + logDefault(request.GetFeatureName()).Infof("set monitoring severity level to %d", handler.monitoringLevel) + return diagnostics.NewSetMonitoringLevelResponse(types.GenericDeviceModelStatusAccepted), nil +} + +func (handler *ChargingStationHandler) OnSetVariableMonitoring(request *diagnostics.SetVariableMonitoringRequest) (response *diagnostics.SetVariableMonitoringResponse, err error) { + setMonitoringResult := make([]diagnostics.SetMonitoringResult, len(request.MonitoringData)) + for i, req := range request.MonitoringData { + // TODO: configure custom monitoring rules internal for the received SetMonitoringData parameters + logDefault(request.GetFeatureName()).Infof("set monitoring for component %v, variable %v to type %v = %v, severity %v", + req.Component.Name, req.Variable.Name, req.Type, req.Value, req.Severity) + res := diagnostics.SetMonitoringResult{ + ID: req.ID, + Status: diagnostics.SetMonitoringStatusAccepted, + Type: req.Type, + Severity: req.Severity, + Component: req.Component, + Variable: req.Variable, + } + setMonitoringResult[i] = res + } + return diagnostics.NewSetVariableMonitoringResponse(setMonitoringResult), nil +} diff --git a/example/2.1/chargingstation/display_handler.go b/example/2.1/chargingstation/display_handler.go new file mode 100644 index 00000000..21f8bccd --- /dev/null +++ b/example/2.1/chargingstation/display_handler.go @@ -0,0 +1,21 @@ +package main + +import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/display" + +func (handler *ChargingStationHandler) OnClearDisplay(request *display.ClearDisplayRequest) (response *display.ClearDisplayResponse, err error) { + logDefault(request.GetFeatureName()).Infof("cleared display message %v", request.ID) + response = display.NewClearDisplayResponse(display.ClearMessageStatusAccepted) + return +} + +func (handler *ChargingStationHandler) OnGetDisplayMessages(request *display.GetDisplayMessagesRequest) (response *display.GetDisplayMessagesResponse, err error) { + logDefault(request.GetFeatureName()).Infof("request %v to send display messages ignored", request.RequestID) + response = display.NewGetDisplayMessagesResponse(display.MessageStatusUnknown) + return +} + +func (handler *ChargingStationHandler) OnSetDisplayMessage(request *display.SetDisplayMessageRequest) (response *display.SetDisplayMessageResponse, err error) { + logDefault(request.GetFeatureName()).Infof("accepted request to display message %v: %v", request.Message.ID, request.Message.Message.Content) + response = display.NewSetDisplayMessageResponse(display.DisplayMessageStatusAccepted) + return +} diff --git a/example/2.1/chargingstation/firmware_handler.go b/example/2.1/chargingstation/firmware_handler.go new file mode 100644 index 00000000..bd64b32d --- /dev/null +++ b/example/2.1/chargingstation/firmware_handler.go @@ -0,0 +1,76 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "io" + "net/http" + "os" + "time" +) + +func (handler *ChargingStationHandler) OnPublishFirmware(request *firmware.PublishFirmwareRequest) (response *firmware.PublishFirmwareResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnUnpublishFirmware(request *firmware.UnpublishFirmwareRequest) (response *firmware.UnpublishFirmwareResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnUpdateFirmware(request *firmware.UpdateFirmwareRequest) (response *firmware.UpdateFirmwareResponse, err error) { + retries := 0 + retryInterval := 30 + if request.Retries != nil { + retries = *request.Retries + } + if request.RetryInterval != nil { + retryInterval = *request.RetryInterval + } + logDefault(request.GetFeatureName()).Infof("starting update firmware procedure") + go updateFirmware(request.Firmware.Location, request.Firmware.RetrieveDateTime, request.Firmware.InstallDateTime, retries, retryInterval) + return firmware.NewUpdateFirmwareResponse(firmware.UpdateFirmwareStatusAccepted), nil +} + +func updateFirmwareStatus(status firmware.FirmwareStatus, props ...func(request *firmware.FirmwareStatusNotificationRequest)) { + statusConfirmation, err := chargingStation.FirmwareStatusNotification(status, props...) + checkError(err) + logDefault(statusConfirmation.GetFeatureName()).Infof("firmware status updated to %v", status) +} + +// Retrieve data and install date are ignored for this test function. +func updateFirmware(location string, retrieveDate *types.DateTime, installDate *types.DateTime, retries int, retryInterval int) { + updateFirmwareStatus(firmware.FirmwareStatusDownloading) + err := downloadFile("/tmp/out.bin", location) + if err != nil { + logDefault(firmware.UpdateFirmwareFeatureName).Errorf("error while downloading file %v", err) + updateFirmwareStatus(firmware.FirmwareStatusDownloadFailed) + return + } + updateFirmwareStatus(firmware.FirmwareStatusDownloaded) + // Simulate installation + updateFirmwareStatus(firmware.FirmwareStatusInstalling) + time.Sleep(time.Second * 5) + // Notify completion + updateFirmwareStatus(firmware.FirmwareStatusInstalled) +} + +func downloadFile(filepath string, url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/example/2.1/chargingstation/handler.go b/example/2.1/chargingstation/handler.go new file mode 100644 index 00000000..030b67c2 --- /dev/null +++ b/example/2.1/chargingstation/handler.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "time" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" +) + +// ConnectorInfo contains some simple state about a single connector. +type ConnectorInfo struct { + status availability.ConnectorStatus + availability availability.OperationalStatus + typ reservation.ConnectorType +} + +// EVSEInfo contains some simple state +type EVSEInfo struct { + availability availability.OperationalStatus + currentTransaction string + currentReservation int + connectors map[int]ConnectorInfo + seqNo int +} + +// ChargingStationHandler contains some simple state that a charge point needs to keep. +// In production this will typically be replaced by database/API calls. +type ChargingStationHandler struct { + availability availability.OperationalStatus + evse map[int]*EVSEInfo + configuration map[string]string + model string + vendor string + meterValue float64 + localAuthList []localauth.AuthorizationData + localAuthListVersion int + monitoringLevel int +} + +var chargingStation ocpp2.ChargingStation + +func (evse *EVSEInfo) hasConnector(ID int) bool { + return ID > 0 && len(evse.connectors) > ID +} + +func (evse *EVSEInfo) getConnectorOfType(typ reservation.ConnectorType) int { + for i, c := range evse.connectors { + if c.typ == typ { + return i + } + } + return -1 +} + +func (evse *EVSEInfo) nextSequence() int { + seq := evse.seqNo + evse.seqNo = seq + 1 + return seq +} + +var transactionID = 0 + +func nextTransactionID() string { + transactionID++ + return fmt.Sprintf("%d", transactionID) +} + +func checkError(err error) { + if err != nil { + log.Fatal(err) + } +} + +func getExpiryDate(info *types.IdTokenInfo) string { + if info.CacheExpiryDateTime != nil { + return fmt.Sprintf("authorized until %v", info.CacheExpiryDateTime.String()) + } + return "" +} + +func updateOperationalStatus(stateHandler *ChargingStationHandler, evseID int, status availability.OperationalStatus) { + if evseID == 0 { + stateHandler.availability = status + log.Infof("operational status for charging station updated to: %v", status) + } else if evse, ok := stateHandler.evse[evseID]; !ok { + log.Errorf("couldn't update operational status for invalid evse %d", evseID) + return + } else { + evse.availability = status + log.Infof("operational status for evse %d updated to: %v", evseID, status) + } +} + +func updateConnectorStatus(stateHandler *ChargingStationHandler, evseID int, connector int, status availability.ConnectorStatus) { + if evse, ok := stateHandler.evse[evseID]; !ok { + log.Errorf("couldn't update connector status for invalid evse %d", evseID) + return + } else if connector < 0 || connector > len(evse.connectors) { + log.Errorf("couldn't update status for evse %d with invalid connector %d", evseID, connector) + return + } else { + conn := evse.connectors[connector] + conn.status = status + evse.connectors[connector] = conn + // Send asynchronous status update + response, err := chargingStation.StatusNotification(types.NewDateTime(time.Now()), status, evseID, connector) + checkError(err) + logDefault(response.GetFeatureName()).Infof("status for evse %d - connector %d updated to: %v", evseID, connector, status) + } +} diff --git a/example/2.1/chargingstation/iso15118_handler.go b/example/2.1/chargingstation/iso15118_handler.go new file mode 100644 index 00000000..5987743e --- /dev/null +++ b/example/2.1/chargingstation/iso15118_handler.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/iso15118" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (handler *ChargingStationHandler) OnDeleteCertificate(request *iso15118.DeleteCertificateRequest) (response *iso15118.DeleteCertificateResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnGetInstalledCertificateIds(request *iso15118.GetInstalledCertificateIdsRequest) (response *iso15118.GetInstalledCertificateIdsResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnInstallCertificate(request *iso15118.InstallCertificateRequest) (response *iso15118.InstallCertificateResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} diff --git a/example/2.1/chargingstation/localauth_handler.go b/example/2.1/chargingstation/localauth_handler.go new file mode 100644 index 00000000..ed258f4b --- /dev/null +++ b/example/2.1/chargingstation/localauth_handler.go @@ -0,0 +1,26 @@ +package main + +import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/localauth" + +func (handler *ChargingStationHandler) OnGetLocalListVersion(request *localauth.GetLocalListVersionRequest) (response *localauth.GetLocalListVersionResponse, err error) { + logDefault(request.GetFeatureName()).Infof("returning current local list version: %v", handler.localAuthListVersion) + return localauth.NewGetLocalListVersionResponse(handler.localAuthListVersion), nil +} + +func (handler *ChargingStationHandler) OnSendLocalList(request *localauth.SendLocalListRequest) (response *localauth.SendLocalListResponse, err error) { + if request.VersionNumber <= handler.localAuthListVersion { + logDefault(request.GetFeatureName()). + Errorf("requested listVersion %v is lower/equal than the current list version %v", request.VersionNumber, handler.localAuthListVersion) + return localauth.NewSendLocalListResponse(localauth.SendLocalListStatusVersionMismatch), nil + } + if request.UpdateType == localauth.UpdateTypeFull { + handler.localAuthList = request.LocalAuthorizationList + handler.localAuthListVersion = request.VersionNumber + } else if request.UpdateType == localauth.UpdateTypeDifferential { + handler.localAuthList = append(handler.localAuthList, request.LocalAuthorizationList...) + handler.localAuthListVersion = request.VersionNumber + } + logDefault(request.GetFeatureName()).Errorf("accepted new local authorization list %v, %v", + request.VersionNumber, request.UpdateType) + return localauth.NewSendLocalListResponse(localauth.SendLocalListStatusAccepted), nil +} diff --git a/example/2.1/chargingstation/provisioning_handler.go b/example/2.1/chargingstation/provisioning_handler.go new file mode 100644 index 00000000..9f47b5b7 --- /dev/null +++ b/example/2.1/chargingstation/provisioning_handler.go @@ -0,0 +1,76 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (handler *ChargingStationHandler) OnGetBaseReport(request *provisioning.GetBaseReportRequest) (response *provisioning.GetBaseReportResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnGetReport(request *provisioning.GetReportRequest) (response *provisioning.GetReportResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnGetVariables(request *provisioning.GetVariablesRequest) (response *provisioning.GetVariablesResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnReset(request *provisioning.ResetRequest) (response *provisioning.ResetResponse, err error) { + logDefault(request.GetFeatureName()).Info("reset handled") + response = provisioning.NewResetResponse(provisioning.ResetStatusAccepted) + return +} + +func (handler *ChargingStationHandler) OnSetNetworkProfile(request *provisioning.SetNetworkProfileRequest) (response *provisioning.SetNetworkProfileResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnSetVariables(request *provisioning.SetVariablesRequest) (response *provisioning.SetVariablesResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +//func (handler *ChargingStationHandler) OnChangeConfiguration(request *core.ChangeConfigurationRequest) (confirmation *core.ChangeConfigurationConfirmation, err error) { +// configKey, ok := handler.configuration[request.Key] +// if !ok { +// logDefault(request.GetFeatureName()).Errorf("couldn't change configuration for unsupported parameter %v", configKey.Key) +// return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusNotSupported), nil +// } else if configKey.Readonly { +// logDefault(request.GetFeatureName()).Errorf("couldn't change configuration for readonly parameter %v", configKey.Key) +// return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusRejected), nil +// } +// configKey.Value = &request.Value +// handler.configuration[request.Key] = configKey +// logDefault(request.GetFeatureName()).Infof("changed configuration for parameter %v to %v", configKey.Key, configKey.Value) +// return core.NewChangeConfigurationConfirmation(core.ConfigurationStatusAccepted), nil +//} +// +//func (handler *ChargingStationHandler) OnGetConfiguration(request *core.GetConfigurationRequest) (confirmation *core.GetConfigurationConfirmation, err error) { +// var resultKeys []core.ConfigurationKey +// var unknownKeys []string +// for _, key := range request.Key { +// configKey, ok := handler.configuration[key] +// if !ok { +// unknownKeys = append(unknownKeys, *configKey.Value) +// } else { +// resultKeys = append(resultKeys, configKey) +// } +// } +// if len(request.Key) == 0 { +// // Return config for all keys∂ +// for _, v := range handler.configuration { +// resultKeys = append(resultKeys, v) +// } +// } +// logDefault(request.GetFeatureName()).Infof("returning configuration for requested keys: %v", request.Key) +// conf := core.NewGetConfigurationConfirmation(resultKeys) +// conf.UnknownKey = unknownKeys +// return conf, nil +//} diff --git a/example/2.1/chargingstation/remotecontrol_handler.go b/example/2.1/chargingstation/remotecontrol_handler.go new file mode 100644 index 00000000..73ed14df --- /dev/null +++ b/example/2.1/chargingstation/remotecontrol_handler.go @@ -0,0 +1,146 @@ +package main + +import ( + "math/rand" + "time" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/diagnostics" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/remotecontrol" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" +) + +func (handler *ChargingStationHandler) OnRequestStartTransaction(request *remotecontrol.RequestStartTransactionRequest) (response *remotecontrol.RequestStartTransactionResponse, err error) { + if request.EvseID != nil { + evse, ok := handler.evse[*request.EvseID] + if !ok || evse.availability != availability.OperationalStatusOperative { + return remotecontrol.NewRequestStartTransactionResponse(remotecontrol.RequestStartStopStatusRejected), nil + } + // Find occupied connector + connectorID := 0 + for i, c := range evse.connectors { + if c.status == availability.ConnectorStatusOccupied { + connectorID = i + break + } + } + evse.currentTransaction = nextTransactionID() + logDefault(request.GetFeatureName()).Infof("started transaction %v on evse %v, connector %v", evse.currentTransaction, *request.EvseID, connectorID) + response = remotecontrol.NewRequestStartTransactionResponse(remotecontrol.RequestStartStopStatusAccepted) + response.TransactionID = evse.currentTransaction + return response, nil + } + logDefault(request.GetFeatureName()).Errorf("couldn't start a transaction for token %v without an evseID", request.IDToken) + return remotecontrol.NewRequestStartTransactionResponse(remotecontrol.RequestStartStopStatusRejected), nil +} + +func (handler *ChargingStationHandler) OnRequestStopTransaction(request *remotecontrol.RequestStopTransactionRequest) (response *remotecontrol.RequestStopTransactionResponse, err error) { + for key, evse := range handler.evse { + if evse.currentTransaction == request.TransactionID { + logDefault(request.GetFeatureName()).Infof("stopped transaction %v on evse %v", evse.currentTransaction, key) + evse.currentTransaction = "" + evse.currentReservation = 0 + // Find the currently occupied connector + for i, c := range evse.connectors { + if c.status == availability.ConnectorStatusOccupied { + connector := evse.connectors[i] + connector.status = availability.ConnectorStatusAvailable + evse.connectors[i] = connector + break + } + } + return remotecontrol.NewRequestStopTransactionResponse(remotecontrol.RequestStartStopStatusAccepted), nil + } + } + logDefault(request.GetFeatureName()).Errorf("couldn't stop transaction %v, no such transaction is ongoing", request.TransactionID) + return remotecontrol.NewRequestStopTransactionResponse(remotecontrol.RequestStartStopStatusRejected), nil +} + +func (handler *ChargingStationHandler) OnTriggerMessage(request *remotecontrol.TriggerMessageRequest) (response *remotecontrol.TriggerMessageResponse, err error) { + logDefault(request.GetFeatureName()).Infof("received trigger for %v", request.RequestedMessage) + status := remotecontrol.TriggerMessageStatusRejected + switch request.RequestedMessage { + case remotecontrol.MessageTriggerBootNotification: + // Boot Notification + go func() { + _, e := chargingStation.BootNotification(provisioning.BootReasonTriggered, handler.model, handler.vendor) + checkError(e) + logDefault(provisioning.BootNotificationFeatureName).Info("boot notification completed") + }() + status = remotecontrol.TriggerMessageStatusAccepted + case remotecontrol.MessageTriggerLogStatusNotification: + // Log Status Notification + go func() { + reqID := rand.Int() + _, e := chargingStation.LogStatusNotification(diagnostics.UploadLogStatusUploading, reqID) + checkError(e) + logDefault(diagnostics.LogStatusNotificationFeatureName).Info("diagnostics status notified") + }() + status = remotecontrol.TriggerMessageStatusAccepted + case remotecontrol.MessageTriggerFirmwareStatusNotification: + //TODO: schedule firmware status notification message + status = remotecontrol.TriggerMessageStatusAccepted + case remotecontrol.MessageTriggerHeartbeat: + // Schedule heartbeat request + go func() { + resp, e := chargingStation.Heartbeat() + checkError(e) + logDefault(availability.HeartbeatFeatureName).Infof("clock synchronized: %v", resp.CurrentTime.FormatTimestamp()) + }() + status = remotecontrol.TriggerMessageStatusAccepted + case remotecontrol.MessageTriggerMeterValues: + // Schedule meter values update + //TODO: schedule meter values message + break + case remotecontrol.MessageTriggerStatusNotification: + // Schedule connector status notification + if request.Evse != nil { + connectorStatus := availability.ConnectorStatusUnavailable + evse, ok := handler.evse[request.Evse.ID] + if !ok { + status = remotecontrol.TriggerMessageStatusRejected + break + } + if request.Evse.ConnectorID != nil { + if !evse.hasConnector(*request.Evse.ConnectorID) { + status = remotecontrol.TriggerMessageStatusRejected + break + } + connectorStatus = evse.connectors[*request.Evse.ConnectorID].status + } else { + status = remotecontrol.TriggerMessageStatusRejected + break + } + // Update asynchronously + go func() { + _, e := chargingStation.StatusNotification(types.NewDateTime(time.Now()), connectorStatus, request.Evse.ID, *request.Evse.ConnectorID) + checkError(e) + logDefault(availability.HeartbeatFeatureName).Infof("status for connector %v sent: %v", *request.Evse.ConnectorID, connectorStatus) + }() + status = remotecontrol.TriggerMessageStatusAccepted + } else { + status = remotecontrol.TriggerMessageStatusRejected + } + case remotecontrol.MessageTriggerTransactionEvent: + // TODO: + break + default: + // We're not implementing support for other messages + status = remotecontrol.TriggerMessageStatusNotImplemented + } + return remotecontrol.NewTriggerMessageResponse(status), nil +} + +func (handler *ChargingStationHandler) OnUnlockConnector(request *remotecontrol.UnlockConnectorRequest) (response *remotecontrol.UnlockConnectorResponse, err error) { + evse, ok := handler.evse[request.EvseID] + if !ok || !evse.hasConnector(request.ConnectorID) { + logDefault(request.GetFeatureName()).Errorf("couldn't unlock unknown connector %d for EVSE %d", request.ConnectorID, request.EvseID) + return remotecontrol.NewUnlockConnectorResponse(remotecontrol.UnlockStatusUnknownConnector), nil + } + connector := evse.connectors[request.ConnectorID] + // TODO: unlock connector internally + connector.status = availability.ConnectorStatusAvailable + logDefault(request.GetFeatureName()).Infof("unlocked connector %v for EVSE %d", request.ConnectorID, request.EvseID) + return remotecontrol.NewUnlockConnectorResponse(remotecontrol.UnlockStatusUnlocked), nil +} diff --git a/example/2.1/chargingstation/reservation_handler.go b/example/2.1/chargingstation/reservation_handler.go new file mode 100644 index 00000000..0ad71c50 --- /dev/null +++ b/example/2.1/chargingstation/reservation_handler.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" +) + +func (handler *ChargingStationHandler) OnCancelReservation(request *reservation.CancelReservationRequest) (resp *reservation.CancelReservationResponse, err error) { + for i, e := range handler.evse { + if e.currentReservation == request.ReservationID { + // Found reservation -> cancel + e.currentReservation = -1 + for j := range e.connectors { + if e.connectors[j].status == availability.ConnectorStatusReserved { + go updateConnectorStatus(handler, i, j, availability.ConnectorStatusAvailable) + break + } + } + logDefault(request.GetFeatureName()).Infof("reservation %v for evse %d canceled", request.ReservationID, i) + resp = reservation.NewCancelReservationResponse(reservation.CancelReservationStatusAccepted) + return + } + } + // Didn't find reservation -> reject + logDefault(request.GetFeatureName()).Infof("couldn't cancel reservation %v: reservation not found!", request.ReservationID) + resp = reservation.NewCancelReservationResponse(reservation.CancelReservationStatusRejected) + return +} + +func (handler *ChargingStationHandler) OnReserveNow(request *reservation.ReserveNowRequest) (resp *reservation.ReserveNowResponse, err error) { + var reservedEvse int + var reservedConnector int + var status reservation.ReserveNowStatus + + status, reservedEvse, reservedConnector, err = handler.findConnector(request.EvseID, request.ConnectorType) + if err != nil { + logDefault(request.GetFeatureName()).Error(err) + } + resp = reservation.NewReserveNowResponse(status) + if resp.Status != reservation.ReserveNowStatusAccepted { + resp.StatusInfo = types.NewStatusInfo("code", err.Error()) + return + } + // Complete reservation + evse := handler.evse[reservedEvse] + evse.currentReservation = request.ID + logDefault(request.GetFeatureName()).Infof("reservation %v accepted for evse %v, connector %v", + request.ID, reservedEvse, reservedConnector) + go updateConnectorStatus(handler, reservedEvse, reservedConnector, availability.ConnectorStatusReserved) + + // TODO: the logic above is incomplete. Advanced support for reservation management is missing. + // TODO: automatically remove reservation after expiryDate + return +} + +func (handler *ChargingStationHandler) findConnector(requestedEVSE *int, connectorType reservation.ConnectorType) (status reservation.ReserveNowStatus, evseID int, connectorID int, err error) { + status = reservation.ReserveNowStatusAccepted + if requestedEVSE != nil { + evseID = *requestedEVSE + evse, ok := handler.evse[evseID] + if !ok { + status = reservation.ReserveNowStatusRejected + err = fmt.Errorf("couldn't reserve a connector for invalid evse %d", evseID) + return + } else if evse.currentReservation != 0 { + status = reservation.ReserveNowStatusOccupied + err = fmt.Errorf("evse %v already has a pending reservation", evseID) + return + } else if evse.availability == availability.OperationalStatusInoperative { + status = reservation.ReserveNowStatusUnavailable + err = fmt.Errorf("evse %v is currently not operative", evseID) + return + } + for i, c := range evse.connectors { + if connectorType != "" && c.typ != connectorType { + continue + } + switch c.status { + case availability.ConnectorStatusReserved, availability.ConnectorStatusOccupied: + status = reservation.ReserveNowStatusOccupied + case availability.ConnectorStatusUnavailable: + status = reservation.ReserveNowStatusUnavailable + case availability.ConnectorStatusFaulted: + status = reservation.ReserveNowStatusUnavailable + case availability.ConnectorStatusAvailable: + // Found an available connector + status = reservation.ReserveNowStatusAccepted + connectorID = i + return + } + } + } else { + // Find suitable evse + connector + for j, e := range handler.evse { + evseID = j + if e.currentReservation != 0 { + status = reservation.ReserveNowStatusOccupied + err = fmt.Errorf("evse %v already has a pending reservation", evseID) + return + } else if e.availability == availability.OperationalStatusInoperative { + status = reservation.ReserveNowStatusUnavailable + err = fmt.Errorf("evse %v is currently not operative", evseID) + return + } + for i, c := range e.connectors { + if connectorType != "" && c.typ != connectorType { + continue + } + switch c.status { + case availability.ConnectorStatusReserved, availability.ConnectorStatusOccupied: + status = reservation.ReserveNowStatusOccupied + case availability.ConnectorStatusUnavailable: + status = reservation.ReserveNowStatusUnavailable + case availability.ConnectorStatusFaulted: + status = reservation.ReserveNowStatusUnavailable + case availability.ConnectorStatusAvailable: + // Found an available connector + status = reservation.ReserveNowStatusAccepted + connectorID = i + return + } + } + } + if status == "" { + status = reservation.ReserveNowStatusRejected + err = fmt.Errorf("no available evse found") + } + } + return +} diff --git a/example/2.1/chargingstation/smartcharging_handler.go b/example/2.1/chargingstation/smartcharging_handler.go new file mode 100644 index 00000000..ac702259 --- /dev/null +++ b/example/2.1/chargingstation/smartcharging_handler.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (handler *ChargingStationHandler) OnClearChargingProfile(request *smartcharging.ClearChargingProfileRequest) (response *smartcharging.ClearChargingProfileResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnGetChargingProfiles(request *smartcharging.GetChargingProfilesRequest) (response *smartcharging.GetChargingProfilesResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnGetCompositeSchedule(request *smartcharging.GetCompositeScheduleRequest) (response *smartcharging.GetCompositeScheduleResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (handler *ChargingStationHandler) OnSetChargingProfile(request *smartcharging.SetChargingProfileRequest) (response *smartcharging.SetChargingProfileResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} diff --git a/example/2.1/chargingstation/tariffcost_handler.go b/example/2.1/chargingstation/tariffcost_handler.go new file mode 100644 index 00000000..acfc10e5 --- /dev/null +++ b/example/2.1/chargingstation/tariffcost_handler.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/tariffcost" +) + +func (handler *ChargingStationHandler) OnCostUpdated(request *tariffcost.CostUpdatedRequest) (response *tariffcost.CostUpdatedResponse, err error) { + logDefault(request.GetFeatureName()).Infof("accepted request to display cost for transaction %v: %v", request.TransactionID, request.TotalCost) + // TODO: update internal display to show updated cost for transaction + return tariffcost.NewCostUpdatedResponse(), nil +} diff --git a/example/2.1/chargingstation/transactions_handler.go b/example/2.1/chargingstation/transactions_handler.go new file mode 100644 index 00000000..f2975fcd --- /dev/null +++ b/example/2.1/chargingstation/transactions_handler.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/transactions" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (handler *ChargingStationHandler) OnGetTransactionStatus(request *transactions.GetTransactionStatusRequest) (response *transactions.GetTransactionStatusResponse, err error) { + logDefault(request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} diff --git a/example/2.1/create-test-certificates.sh b/example/2.1/create-test-certificates.sh new file mode 100755 index 00000000..25084c73 --- /dev/null +++ b/example/2.1/create-test-certificates.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +mkdir -p certs/csms +mkdir -p certs/chargingstation +cd certs +# Create CA +openssl req -new -x509 -nodes -sha256 -days 120 -extensions v3_ca -keyout ca.key -out ca.crt -subj "/CN=ocpp-go-example" +# Generate self-signed CSMS certificate +openssl genrsa -out csms/csms.key 4096 +openssl req -new -out csms/csms.csr -key csms/csms.key -config ../openssl-csms.conf -sha256 +openssl x509 -req -in csms/csms.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out csms/csms.crt -days 120 -extensions req_ext -extfile ../openssl-csms.conf -sha256 +# Generate self-signed charging-station certificate +openssl genrsa -out chargingstation/charging-station.key 4096 +openssl req -new -out chargingstation/charging-station.csr -key chargingstation/charging-station.key -config ../openssl-chargingstation.conf -sha256 +openssl x509 -req -in chargingstation/charging-station.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out chargingstation/charging-station.crt -days 120 -extensions req_ext -extfile ../openssl-chargingstation.conf -sha256 diff --git a/example/2.1/csms/Dockerfile b/example/2.1/csms/Dockerfile new file mode 100644 index 00000000..ee1d98a2 --- /dev/null +++ b/example/2.1/csms/Dockerfile @@ -0,0 +1,25 @@ +############################ +# STEP 1 build executable binary +############################ +FROM golang:alpine AS builder + +ENV GO111MODULE on +WORKDIR $GOPATH/src/github.com/lorenzodonini/ocpp-go +COPY . . +# Fetch dependencies. +RUN go mod download +# Build the binary. +RUN go build -ldflags="-w -s" -o /go/bin/csms example/2.1/csms/*.go + +############################ +# STEP 2 build a small image +############################ +FROM alpine + +COPY --from=builder /go/bin/csms /bin/csms + +# Ports on which the service may be exposed. +EXPOSE 8887 +EXPOSE 443 + +CMD [ "csms" ] diff --git a/example/2.1/csms/authorization_handler.go b/example/2.1/csms/authorization_handler.go new file mode 100644 index 00000000..1f93f896 --- /dev/null +++ b/example/2.1/csms/authorization_handler.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/authorization" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" +) + +func (c *CSMSHandler) OnAuthorize(chargingStationID string, request *authorization.AuthorizeRequest) (response *authorization.AuthorizeResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("client with token %v authorized", request.IdToken) + response = authorization.NewAuthorizationResponse(*types.NewIdTokenInfo(types.AuthorizationStatusAccepted)) + return +} diff --git a/example/2.1/csms/availability_handler.go b/example/2.1/csms/availability_handler.go new file mode 100644 index 00000000..962842c0 --- /dev/null +++ b/example/2.1/csms/availability_handler.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "time" +) + +func (c *CSMSHandler) OnHeartbeat(chargingStationID string, request *availability.HeartbeatRequest) (response *availability.HeartbeatResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("heartbeat handled") + response = availability.NewHeartbeatResponse(types.DateTime{Time: time.Now()}) + return +} + +func (c *CSMSHandler) OnStatusNotification(chargingStationID string, request *availability.StatusNotificationRequest) (response *availability.StatusNotificationResponse, err error) { + info, ok := c.chargingStations[chargingStationID] + if !ok { + return nil, fmt.Errorf("unknown charging station %v", chargingStationID) + } + if request.ConnectorID > 0 { + connectorInfo := info.getConnector(request.ConnectorID) + connectorInfo.status = request.ConnectorStatus + logDefault(chargingStationID, request.GetFeatureName()).Infof("connector %v updated status to %v", request.ConnectorID, request.ConnectorStatus) + } else { + logDefault(chargingStationID, request.GetFeatureName()).Infof("couldn't update status for invalid connector %v", request.ConnectorID) + } + response = availability.NewStatusNotificationResponse() + return +} diff --git a/example/2.1/csms/csms_sim.go b/example/2.1/csms/csms_sim.go new file mode 100644 index 00000000..57b88ec9 --- /dev/null +++ b/example/2.1/csms/csms_sim.go @@ -0,0 +1,302 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "strconv" + "time" + + "github.com/sirupsen/logrus" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/diagnostics" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/display" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/localauth" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/remotecontrol" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/reservation" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "github.com/lorenzodonini/ocpp-go/ocppj" + "github.com/lorenzodonini/ocpp-go/ws" +) + +const ( + defaultListenPort = 8887 + defaultHeartbeatInterval = 600 + envVarServerPort = "SERVER_LISTEN_PORT" + envVarTls = "TLS_ENABLED" + envVarCaCertificate = "CA_CERTIFICATE_PATH" + envVarServerCertificate = "SERVER_CERTIFICATE_PATH" + envVarServerCertificateKey = "SERVER_CERTIFICATE_KEY_PATH" +) + +var log *logrus.Logger +var csms ocpp2.CSMS + +func setupCentralSystem() ocpp2.CSMS { + return ocpp2.NewCSMS(nil, nil) +} + +func setupTlsCentralSystem() ocpp2.CSMS { + var certPool *x509.CertPool + // Load CA certificates + caCertificate, ok := os.LookupEnv(envVarCaCertificate) + if !ok { + log.Infof("no %v found, using system CA pool", envVarCaCertificate) + systemPool, err := x509.SystemCertPool() + if err != nil { + log.Fatalf("couldn't get system CA pool: %v", err) + } + certPool = systemPool + } else { + certPool = x509.NewCertPool() + data, err := os.ReadFile(caCertificate) + if err != nil { + log.Fatalf("couldn't read CA certificate from %v: %v", caCertificate, err) + } + ok = certPool.AppendCertsFromPEM(data) + if !ok { + log.Fatalf("couldn't read CA certificate from %v", caCertificate) + } + } + certificate, ok := os.LookupEnv(envVarServerCertificate) + if !ok { + log.Fatalf("no required %v found", envVarServerCertificate) + } + key, ok := os.LookupEnv(envVarServerCertificateKey) + if !ok { + log.Fatalf("no required %v found", envVarServerCertificateKey) + } + server := ws.NewServer(ws.WithServerTLSConfig(certificate, key, &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, + })) + return ocpp2.NewCSMS(nil, server) +} + +// Run for every connected Charging Station, to simulate some functionality +func exampleRoutine(chargingStationID string, handler *CSMSHandler) { + // Wait for some time + time.Sleep(2 * time.Second) + // Reserve a connector + reservationID := 42 + clientIDTokenType := types.IdToken{IdToken: "1234", Type: types.IdTokenTypeKeyCode} + clientIdTag := "l33t" + connectorID := 1 + expiryDate := types.NewDateTime(time.Now().Add(1 * time.Hour)) + cb1 := func(confirmation *reservation.ReserveNowResponse, err error) { + if err != nil { + logDefault(chargingStationID, reservation.ReserveNowFeatureName).Errorf("error on request: %v", err) + } else if confirmation.Status == reservation.ReserveNowStatusAccepted { + logDefault(chargingStationID, confirmation.GetFeatureName()).Infof("connector %v reserved for client %v until %v (reservation ID %d)", connectorID, clientIdTag, expiryDate.FormatTimestamp(), reservationID) + } else { + logDefault(chargingStationID, confirmation.GetFeatureName()).Infof("couldn't reserve connector %v: %v", connectorID, confirmation.Status) + } + } + e := csms.ReserveNow(chargingStationID, cb1, reservationID, expiryDate, clientIDTokenType) + if e != nil { + logDefault(chargingStationID, reservation.ReserveNowFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Wait for some time + time.Sleep(1 * time.Second) + // Cancel the reservation + cb2 := func(confirmation *reservation.CancelReservationResponse, err error) { + if err != nil { + logDefault(chargingStationID, reservation.CancelReservationFeatureName).Errorf("error on request: %v", err) + } else if confirmation.Status == reservation.CancelReservationStatusAccepted { + logDefault(chargingStationID, confirmation.GetFeatureName()).Infof("reservation %v canceled successfully", reservationID) + } else { + logDefault(chargingStationID, confirmation.GetFeatureName()).Infof("couldn't cancel reservation %v", reservationID) + } + } + e = csms.CancelReservation(chargingStationID, cb2, reservationID) + if e != nil { + logDefault(chargingStationID, reservation.ReserveNowFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Wait for some time + time.Sleep(5 * time.Second) + // Get current local list version + cb3 := func(confirmation *localauth.GetLocalListVersionResponse, err error) { + if err != nil { + logDefault(chargingStationID, localauth.GetLocalListVersionFeatureName).Errorf("error on request: %v", err) + } else { + logDefault(chargingStationID, confirmation.GetFeatureName()).Infof("current local list version: %v", confirmation.VersionNumber) + } + } + e = csms.GetLocalListVersion(chargingStationID, cb3) + if e != nil { + logDefault(chargingStationID, localauth.GetLocalListVersionFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Wait for some time + time.Sleep(5 * time.Second) + setVariableData := []provisioning.SetVariableData{ + { + AttributeType: types.AttributeTarget, + AttributeValue: "10", + Component: types.Component{Name: "OCPPCommCtrlr"}, + Variable: types.Variable{Name: "HeartbeatInterval"}, + }, + { + AttributeType: types.AttributeTarget, + AttributeValue: "true", + Component: types.Component{Name: "AuthCtrlr"}, + Variable: types.Variable{Name: "Enabled"}, + }, + } + // Change meter sampling values time + cb4 := func(response *provisioning.SetVariablesResponse, err error) { + if err != nil { + logDefault(chargingStationID, provisioning.SetVariablesFeatureName).Errorf("error on request: %v", err) + return + } + for _, r := range response.SetVariableResult { + if r.AttributeStatus == provisioning.SetVariableStatusNotSupported { + logDefault(chargingStationID, response.GetFeatureName()).Warnf("couldn't update variable %v for component %v: unsupported", r.Variable.Name, r.Component.Name) + } else if r.AttributeStatus == provisioning.SetVariableStatusUnknownComponent { + logDefault(chargingStationID, response.GetFeatureName()).Warnf("couldn't update variable for unknown component %v", r.Component.Name) + } else if r.AttributeStatus == provisioning.SetVariableStatusUnknownVariable { + logDefault(chargingStationID, response.GetFeatureName()).Warnf("couldn't update unknown variable %v for component %v", r.Variable.Name, r.Component.Name) + } else if r.AttributeStatus == provisioning.SetVariableStatusRejected { + logDefault(chargingStationID, response.GetFeatureName()).Warnf("couldn't update variable %v for key: %v", r.Variable.Name, r.Component.Name) + } else { + logDefault(chargingStationID, response.GetFeatureName()).Infof("updated variable %v for component %v", r.Variable.Name, r.Component.Name) + } + } + } + e = csms.SetVariables(chargingStationID, cb4, setVariableData) + if e != nil { + logDefault(chargingStationID, localauth.GetLocalListVersionFeatureName).Errorf("couldn't send message: %v", e) + return + } + + // Wait for some time + time.Sleep(5 * time.Second) + // Trigger a heartbeat message + cb5 := func(response *remotecontrol.TriggerMessageResponse, err error) { + if err != nil { + logDefault(chargingStationID, remotecontrol.TriggerMessageFeatureName).Errorf("error on request: %v", err) + } else if response.Status == remotecontrol.TriggerMessageStatusAccepted { + logDefault(chargingStationID, response.GetFeatureName()).Infof("%v triggered successfully", availability.HeartbeatFeatureName) + } else if response.Status == remotecontrol.TriggerMessageStatusRejected { + logDefault(chargingStationID, response.GetFeatureName()).Infof("%v trigger was rejected", availability.HeartbeatFeatureName) + } + } + e = csms.TriggerMessage(chargingStationID, cb5, remotecontrol.MessageTriggerHeartbeat) + if e != nil { + logDefault(chargingStationID, remotecontrol.TriggerMessageFeatureName).Errorf("couldn't send message: %v", e) + return + } + + // Wait for some time + time.Sleep(5 * time.Second) + // Trigger a diagnostics status notification + cb6 := func(response *remotecontrol.TriggerMessageResponse, err error) { + if err != nil { + logDefault(chargingStationID, remotecontrol.TriggerMessageFeatureName).Errorf("error on request: %v", err) + } else if response.Status == remotecontrol.TriggerMessageStatusAccepted { + logDefault(chargingStationID, response.GetFeatureName()).Infof("%v triggered successfully", diagnostics.LogStatusNotificationFeatureName) + } else if response.Status == remotecontrol.TriggerMessageStatusRejected { + logDefault(chargingStationID, response.GetFeatureName()).Infof("%v trigger was rejected", diagnostics.LogStatusNotificationFeatureName) + } + } + e = csms.TriggerMessage(chargingStationID, cb6, remotecontrol.MessageTriggerLogStatusNotification) + if e != nil { + logDefault(chargingStationID, remotecontrol.TriggerMessageFeatureName).Errorf("couldn't send message: %v", e) + return + } + + // Wait for some time + time.Sleep(5 * time.Second) + // Set a custom display message + cb7 := func(response *display.SetDisplayMessageResponse, err error) { + if err != nil { + logDefault(chargingStationID, display.SetDisplayMessageFeatureName).Errorf("error on request: %v", err) + } else if response.Status == display.DisplayMessageStatusAccepted { + logDefault(chargingStationID, response.GetFeatureName()).Info("display message set successfully") + } else { + logDefault(chargingStationID, response.GetFeatureName()).Errorf("failed to set display message: %v", response.Status) + } + } + var currentTx int + for txID := range handler.chargingStations[chargingStationID].transactions { + currentTx = txID + break + } + e = csms.SetDisplayMessage(chargingStationID, cb7, display.MessageInfo{ + ID: 42, + Priority: display.MessagePriorityInFront, + State: display.MessageStateCharging, + TransactionID: fmt.Sprintf("%d", currentTx), + Message: types.MessageContent{ + Format: types.MessageFormatUTF8, + Language: "en-US", + Content: "Hello world!", + }, + }) + if e != nil { + logDefault(chargingStationID, display.SetDisplayMessageFeatureName).Errorf("couldn't send message: %v", e) + return + } + // Finish simulation +} + +// Start function +func main() { + // Load config from ENV + var listenPort = defaultListenPort + port, _ := os.LookupEnv(envVarServerPort) + if p, err := strconv.Atoi(port); err == nil { + listenPort = p + } else { + log.Printf("no valid %v environment variable found, using default port", envVarServerPort) + } + // Check if TLS enabled + t, _ := os.LookupEnv(envVarTls) + tlsEnabled, _ := strconv.ParseBool(t) + // Prepare OCPP 1.6 central system + if tlsEnabled { + csms = setupTlsCentralSystem() + } else { + csms = setupCentralSystem() + } + // Support callbacks for all OCPP 2.0.1 profiles + handler := &CSMSHandler{chargingStations: map[string]*ChargingStationState{}} + csms.SetAuthorizationHandler(handler) + csms.SetAvailabilityHandler(handler) + csms.SetDiagnosticsHandler(handler) + csms.SetFirmwareHandler(handler) + csms.SetLocalAuthListHandler(handler) + csms.SetMeterHandler(handler) + csms.SetProvisioningHandler(handler) + csms.SetRemoteControlHandler(handler) + csms.SetReservationHandler(handler) + csms.SetTariffCostHandler(handler) + csms.SetTransactionsHandler(handler) + // Add handlers for dis/connection of charging stations + csms.SetNewChargingStationHandler(func(chargingStation ocpp2.ChargingStationConnection) { + handler.chargingStations[chargingStation.ID()] = &ChargingStationState{connectors: map[int]*ConnectorInfo{}, transactions: map[int]*TransactionInfo{}} + log.WithField("client", chargingStation.ID()).Info("new charging station connected") + go exampleRoutine(chargingStation.ID(), handler) + }) + csms.SetChargingStationDisconnectedHandler(func(chargingStation ocpp2.ChargingStationConnection) { + log.WithField("client", chargingStation.ID()).Info("charging station disconnected") + delete(handler.chargingStations, chargingStation.ID()) + }) + ocppj.SetLogger(log) + // Run CSMS + log.Infof("starting CSMS on port %v", listenPort) + csms.Start(listenPort, "/{ws}") + log.Info("stopped CSMS") +} + +func init() { + log = logrus.New() + log.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) + log.SetLevel(logrus.InfoLevel) +} diff --git a/example/2.1/csms/data_handler.go b/example/2.1/csms/data_handler.go new file mode 100644 index 00000000..8d0c173e --- /dev/null +++ b/example/2.1/csms/data_handler.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/data" +) + +type DataSample struct { + SampleString string `json:"sample_string"` + SampleValue float64 `json:"sample_value"` +} + +func (c *CSMSHandler) OnDataTransfer(chargingStationID string, request *data.DataTransferRequest) (response *data.DataTransferResponse, err error) { + var dataSample DataSample + err = json.Unmarshal(request.Data.([]byte), &dataSample) + if err != nil { + logDefault(chargingStationID, request.GetFeatureName()). + Errorf("invalid data received: %v", request.Data) + return nil, err + } + logDefault(chargingStationID, request.GetFeatureName()). + Infof("data received: %v, %v", dataSample.SampleString, dataSample.SampleValue) + return data.NewDataTransferResponse(data.DataTransferStatusAccepted), nil +} diff --git a/example/2.1/csms/diagnostics_handler.go b/example/2.1/csms/diagnostics_handler.go new file mode 100644 index 00000000..d6534ffc --- /dev/null +++ b/example/2.1/csms/diagnostics_handler.go @@ -0,0 +1,33 @@ +package main + +import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/diagnostics" + +func (c *CSMSHandler) OnLogStatusNotification(chargingStationID string, request *diagnostics.LogStatusNotificationRequest) (response *diagnostics.LogStatusNotificationResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("log upload status: %v", request.Status) + response = diagnostics.NewLogStatusNotificationResponse() + return +} + +func (c *CSMSHandler) OnNotifyCustomerInformation(chargingStationID string, request *diagnostics.NotifyCustomerInformationRequest) (response *diagnostics.NotifyCustomerInformationResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("data report for request %v: %v", request.RequestID, request.Data) + response = diagnostics.NewNotifyCustomerInformationResponse() + return +} + +func (c *CSMSHandler) OnNotifyEvent(chargingStationID string, request *diagnostics.NotifyEventRequest) (response *diagnostics.NotifyEventResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("report part %v for events:\n", request.SeqNo) + for _, ed := range request.EventData { + logDefault(chargingStationID, request.GetFeatureName()).Infof("%v", ed) + } + response = diagnostics.NewNotifyEventResponse() + return +} + +func (c *CSMSHandler) OnNotifyMonitoringReport(chargingStationID string, request *diagnostics.NotifyMonitoringReportRequest) (response *diagnostics.NotifyMonitoringReportResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("report part %v for monitored variables:\n", request.SeqNo) + for _, md := range request.Monitor { + logDefault(chargingStationID, request.GetFeatureName()).Infof("%v", md) + } + response = diagnostics.NewNotifyMonitoringReportResponse() + return +} diff --git a/example/2.1/csms/display_handler.go b/example/2.1/csms/display_handler.go new file mode 100644 index 00000000..5472b1d8 --- /dev/null +++ b/example/2.1/csms/display_handler.go @@ -0,0 +1,14 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/display" +) + +func (c *CSMSHandler) OnNotifyDisplayMessages(chargingStationID string, request *display.NotifyDisplayMessagesRequest) (response *display.NotifyDisplayMessagesResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("received display messages for request %v:\n", request.RequestID) + for _, msg := range request.MessageInfo { + logDefault(chargingStationID, request.GetFeatureName()).Printf("%v", msg) + } + response = display.NewNotifyDisplayMessagesResponse() + return +} diff --git a/example/2.1/csms/firmware_handler.go b/example/2.1/csms/firmware_handler.go new file mode 100644 index 00000000..ed613efc --- /dev/null +++ b/example/2.1/csms/firmware_handler.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/firmware" +) + +func (c *CSMSHandler) OnFirmwareStatusNotification(chargingStationID string, request *firmware.FirmwareStatusNotificationRequest) (response *firmware.FirmwareStatusNotificationResponse, err error) { + info, ok := c.chargingStations[chargingStationID] + if !ok { + err = fmt.Errorf("unknown charging station %v", chargingStationID) + return + } + info.firmwareStatus = request.Status + logDefault(chargingStationID, request.GetFeatureName()).Infof("updated firmware status to %v", request.Status) + response = firmware.NewFirmwareStatusNotificationResponse() + return +} + +func (c *CSMSHandler) OnPublishFirmwareStatusNotification(chargingStationID string, request *firmware.PublishFirmwareStatusNotificationRequest) (response *firmware.PublishFirmwareStatusNotificationResponse, err error) { + if len(request.Location) > 0 { + logDefault(chargingStationID, request.GetFeatureName()).Infof("firmware download status on local controller: %v, download locations: %v", request.Status, request.Location) + } else { + logDefault(chargingStationID, request.GetFeatureName()).Infof("firmware download status on local controller: %v", request.Status) + } + response = firmware.NewPublishFirmwareStatusNotificationResponse() + return +} diff --git a/example/2.1/csms/handler.go b/example/2.1/csms/handler.go new file mode 100644 index 00000000..e420814d --- /dev/null +++ b/example/2.1/csms/handler.go @@ -0,0 +1,62 @@ +package main + +import ( + "github.com/sirupsen/logrus" + + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/availability" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/firmware" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" +) + +// TransactionInfo contains info about a transaction +type TransactionInfo struct { + id int + startTime *types.DateTime + endTime *types.DateTime + startMeter int + endMeter int + connectorID int + idTag string +} + +func (ti *TransactionInfo) hasTransactionEnded() bool { + return ti.endTime != nil && !ti.endTime.IsZero() +} + +// ConnectorInfo contains status and ongoing transaction ID for a connector +type ConnectorInfo struct { + status availability.ConnectorStatus + currentTransaction int +} + +func (ci *ConnectorInfo) hasTransactionInProgress() bool { + return ci.currentTransaction >= 0 +} + +// ChargingStationState contains some simple state for a connected charging station +type ChargingStationState struct { + status availability.ChangeAvailabilityStatus + firmwareStatus firmware.FirmwareStatus + connectors map[int]*ConnectorInfo + transactions map[int]*TransactionInfo +} + +func (s *ChargingStationState) getConnector(id int) *ConnectorInfo { + ci, ok := s.connectors[id] + if !ok { + ci = &ConnectorInfo{currentTransaction: -1} + s.connectors[id] = ci + } + return ci +} + +// CSMSHandler contains some simple state that a CSMS may want to keep. +// In production this will typically be replaced by database/API calls. +type CSMSHandler struct { + chargingStations map[string]*ChargingStationState +} + +// Utility functions +func logDefault(chargingStationID string, feature string) *logrus.Entry { + return log.WithFields(logrus.Fields{"client": chargingStationID, "message": feature}) +} diff --git a/example/2.1/csms/iso15118_handler.go b/example/2.1/csms/iso15118_handler.go new file mode 100644 index 00000000..73beed29 --- /dev/null +++ b/example/2.1/csms/iso15118_handler.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/iso15118" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (c *CSMSHandler) OnGet15118EVCertificate(chargingStationID string, request *iso15118.Get15118EVCertificateRequest) (response *iso15118.Get15118EVCertificateResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (c *CSMSHandler) OnGetCertificateStatus(chargingStationID string, request *iso15118.GetCertificateStatusRequest) (response *iso15118.GetCertificateStatusResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} diff --git a/example/2.1/csms/meter_handler.go b/example/2.1/csms/meter_handler.go new file mode 100644 index 00000000..d1b58014 --- /dev/null +++ b/example/2.1/csms/meter_handler.go @@ -0,0 +1,12 @@ +package main + +import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/meter" + +func (c *CSMSHandler) OnMeterValues(chargingStationID string, request *meter.MeterValuesRequest) (response *meter.MeterValuesResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("received meter values for EVSE %v. Meter values:\n", request.EvseID) + for _, mv := range request.MeterValue { + logDefault(chargingStationID, request.GetFeatureName()).Printf("%v", mv) + } + response = meter.NewMeterValuesResponse() + return +} diff --git a/example/2.1/csms/provisioning_handler.go b/example/2.1/csms/provisioning_handler.go new file mode 100644 index 00000000..08fff4fc --- /dev/null +++ b/example/2.1/csms/provisioning_handler.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/provisioning" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" + "time" +) + +func (c *CSMSHandler) OnBootNotification(chargingStationID string, request *provisioning.BootNotificationRequest) (response *provisioning.BootNotificationResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("boot confirmed for %v %v, serial: %v, firmare version: %v, reason: %v", + request.ChargingStation.VendorName, request.ChargingStation.Model, request.ChargingStation.SerialNumber, request.ChargingStation.FirmwareVersion, request.Reason) + response = provisioning.NewBootNotificationResponse(types.NewDateTime(time.Now()), defaultHeartbeatInterval, provisioning.RegistrationStatusAccepted) + return +} + +func (c *CSMSHandler) OnNotifyReport(chargingStationID string, request *provisioning.NotifyReportRequest) (response *provisioning.NotifyReportResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("data report %v, seq. %v:\n", request.RequestID, request.SeqNo) + for _, d := range request.ReportData { + logDefault(chargingStationID, request.GetFeatureName()).Printf("%v", d) + } + response = provisioning.NewNotifyReportResponse() + return +} diff --git a/example/2.1/csms/reservation_handler.go b/example/2.1/csms/reservation_handler.go new file mode 100644 index 00000000..be6be7d7 --- /dev/null +++ b/example/2.1/csms/reservation_handler.go @@ -0,0 +1,9 @@ +package main + +import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/reservation" + +func (c *CSMSHandler) OnReservationStatusUpdate(chargingStationID string, request *reservation.ReservationStatusUpdateRequest) (response *reservation.ReservationStatusUpdateResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("updated status of reservation %v to: %v", request.ReservationID, request.Status) + response = reservation.NewReservationStatusUpdateResponse() + return +} diff --git a/example/2.1/csms/security_handler.go b/example/2.1/csms/security_handler.go new file mode 100644 index 00000000..e7f7f55a --- /dev/null +++ b/example/2.1/csms/security_handler.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/security" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (c *CSMSHandler) OnSecurityEventNotification(chargingStationID string, request *security.SecurityEventNotificationRequest) (response *security.SecurityEventNotificationResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Infof("type: %s, info: %s", request.Type, request.TechInfo) + response = security.NewSecurityEventNotificationResponse() + return +} + +func (c *CSMSHandler) OnSignCertificate(chargingStationID string, request *security.SignCertificateRequest) (response *security.SignCertificateResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} diff --git a/example/2.1/csms/smartcharging_handler.go b/example/2.1/csms/smartcharging_handler.go new file mode 100644 index 00000000..f46bfc55 --- /dev/null +++ b/example/2.1/csms/smartcharging_handler.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/lorenzodonini/ocpp-go/ocpp" + "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/smartcharging" + "github.com/lorenzodonini/ocpp-go/ocppj" +) + +func (c *CSMSHandler) OnClearedChargingLimit(chargingStationID string, request *smartcharging.ClearedChargingLimitRequest) (response *smartcharging.ClearedChargingLimitResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (c *CSMSHandler) OnNotifyChargingLimit(chargingStationID string, request *smartcharging.NotifyChargingLimitRequest) (response *smartcharging.NotifyChargingLimitResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (c *CSMSHandler) OnNotifyEVChargingNeeds(chargingStationID string, request *smartcharging.NotifyEVChargingNeedsRequest) (response *smartcharging.NotifyEVChargingNeedsResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (c *CSMSHandler) OnNotifyEVChargingSchedule(chargingStationID string, request *smartcharging.NotifyEVChargingScheduleRequest) (response *smartcharging.NotifyEVChargingScheduleResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} + +func (c *CSMSHandler) OnReportChargingProfiles(chargingStationID string, request *smartcharging.ReportChargingProfilesRequest) (response *smartcharging.ReportChargingProfilesResponse, err error) { + logDefault(chargingStationID, request.GetFeatureName()).Warnf("Unsupported feature") + return nil, ocpp.NewHandlerError(ocppj.NotSupported, "Not supported") +} diff --git a/example/2.1/csms/transactions_handler.go b/example/2.1/csms/transactions_handler.go new file mode 100644 index 00000000..b36ed2b1 --- /dev/null +++ b/example/2.1/csms/transactions_handler.go @@ -0,0 +1,19 @@ +package main + +import "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/transactions" + +func (c *CSMSHandler) OnTransactionEvent(chargingStationID string, request *transactions.TransactionEventRequest) (response *transactions.TransactionEventResponse, err error) { + switch request.EventType { + case transactions.TransactionEventStarted: + logDefault(chargingStationID, request.GetFeatureName()).Infof("transaction %v started, reason: %v, state: %v", request.TransactionInfo.TransactionID, request.TriggerReason, request.TransactionInfo.ChargingState) + case transactions.TransactionEventUpdated: + logDefault(chargingStationID, request.GetFeatureName()).Infof("transaction %v updated, reason: %v, state: %v\n", request.TransactionInfo.TransactionID, request.TriggerReason, request.TransactionInfo.ChargingState) + for _, mv := range request.MeterValue { + logDefault(chargingStationID, request.GetFeatureName()).Printf("%v", mv) + } + case transactions.TransactionEventEnded: + logDefault(chargingStationID, request.GetFeatureName()).Infof("transaction %v stopped, reason: %v, state: %v\n", request.TransactionInfo.TransactionID, request.TriggerReason, request.TransactionInfo.ChargingState) + } + response = transactions.NewTransactionEventResponse() + return +} diff --git a/example/2.1/docker-compose.tls.yml b/example/2.1/docker-compose.tls.yml new file mode 100644 index 00000000..130400b4 --- /dev/null +++ b/example/2.1/docker-compose.tls.yml @@ -0,0 +1,45 @@ +version: '3' +services: + csms: + build: + context: ../.. + dockerfile: csms/Dockerfile + image: ldonini/ocpp2.1-csms:latest + container_name: csms + volumes: + - ./certs/csms:/usr/local/share/certs + - ./certs/ca.crt:/usr/local/share/certs/ca.crt + environment: + - SERVER_LISTEN_PORT=443 + - TLS_ENABLED=true + - CA_CERTIFICATE_PATH=/usr/local/share/certs/ca.crt + - SERVER_CERTIFICATE_PATH=/usr/local/share/certs/csms.crt + - SERVER_CERTIFICATE_KEY_PATH=/usr/local/share/certs/csms.key + ports: + - "443:443" + networks: + - sim + tty: true + charging-station: + build: + context: ../.. + dockerfile: chargingstation/Dockerfile + image: ldonini/ocpp2.1-chargingstation:latest + container_name: charging-station + volumes: + - ./certs/chargingstation:/usr/local/share/certs + - ./certs/ca.crt:/usr/local/share/certs/ca.crt + environment: + - CLIENT_ID=chargingStationSim + - CSMS_URL=wss://csms:443 + - TLS_ENABLED=true + - CA_CERTIFICATE_PATH=/usr/local/share/certs/ca.crt + - CLIENT_CERTIFICATE_PATH=/usr/local/share/certs/charging-station.crt + - CLIENT_CERTIFICATE_KEY_PATH=/usr/local/share/certs/charging-station.key + networks: + - sim + tty: true + +networks: + sim: + driver: bridge diff --git a/example/2.1/docker-compose.yml b/example/2.1/docker-compose.yml new file mode 100644 index 00000000..21441985 --- /dev/null +++ b/example/2.1/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3' +services: + csms: + build: + context: ../.. + dockerfile: csms/Dockerfile + image: ldonini/ocpp2.0.1-csms:latest + container_name: csms + environment: + - SERVER_LISTEN_PORT=8887 + ports: + - "8887:8887" + networks: + - sim + tty: true + charging-station: + build: + context: ../.. + dockerfile: chargingstation/Dockerfile + image: ldonini/ocpp2.0.1-chargingstation:latest + container_name: charging-station + environment: + - CLIENT_ID=chargingStationSim + - CSMS_URL=ws://csms:8887 + networks: + - sim + tty: true + +networks: + sim: + driver: bridge diff --git a/example/2.1/openssl-chargingstation.conf b/example/2.1/openssl-chargingstation.conf new file mode 100644 index 00000000..83d634d4 --- /dev/null +++ b/example/2.1/openssl-chargingstation.conf @@ -0,0 +1,14 @@ +[req] +distinguished_name = req_dn +req_extensions = req_ext +prompt = no +[req_dn] +CN = charging-station +[req_ext] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = charging-station \ No newline at end of file diff --git a/example/2.1/openssl-csms.conf b/example/2.1/openssl-csms.conf new file mode 100644 index 00000000..ff869a47 --- /dev/null +++ b/example/2.1/openssl-csms.conf @@ -0,0 +1,14 @@ +[req] +distinguished_name = req_dn +req_extensions = req_ext +prompt = no +[req_dn] +CN = csms +[req_ext] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +keyUsage = digitalSignature, keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = csms \ No newline at end of file diff --git a/ocppj/charge_point_test.go b/ocppj/charge_point_test.go index 0b0b8ea8..db4efc05 100644 --- a/ocppj/charge_point_test.go +++ b/ocppj/charge_point_test.go @@ -63,7 +63,7 @@ func (suite *OcppJTestSuite) TestClientStoppedError() { // Send message. Expected error time.Sleep(20 * time.Millisecond) call.Return(false) - assert.False(t, suite.clientDispatcher.IsRunning()) + suite.False(suite.clientDispatcher.IsRunning()) req := newMockRequest("somevalue") err = suite.chargePoint.SendRequest(req) assert.Error(t, err, "ocppj client is not started, couldn't send request") @@ -198,7 +198,7 @@ func (suite *OcppJTestSuite) TestChargePointSendRequestFailed() { // Assert that pending request was removed time.Sleep(500 * time.Millisecond) _, ok := suite.chargePoint.RequestState.GetPendingRequest(callID) - assert.False(t, ok) + suite.False(ok) } // ----------------- SendResponse tests ----------------- @@ -657,7 +657,7 @@ func (suite *OcppJTestSuite) TestClientDisconnected() { } // Wait for trigger disconnect after a few responses were returned <-triggerC - assert.False(t, suite.clientDispatcher.IsPaused()) + suite.False(suite.clientDispatcher.IsPaused()) suite.mockClient.DisconnectedHandler(disconnectError) time.Sleep(200 * time.Millisecond) // Not all messages were sent, some are still in queue @@ -716,7 +716,7 @@ func (suite *OcppJTestSuite) TestClientReconnected() { }() // Get the pending request state struct state := suite.chargePoint.RequestState - assert.False(t, state.HasPendingRequest()) + suite.False(state.HasPendingRequest()) // Send some messages for i := 0; i < messagesToQueue; i++ { req := newMockRequest(fmt.Sprintf("%v", i)) @@ -730,22 +730,22 @@ func (suite *OcppJTestSuite) TestClientReconnected() { // 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()) + suite.False(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()) + suite.False(suite.clientDispatcher.IsPaused()) assert.True(t, suite.clientDispatcher.IsRunning()) - assert.False(t, suite.clientRequestQueue.IsEmpty()) + suite.False(suite.clientRequestQueue.IsEmpty()) assert.True(t, suite.chargePoint.IsConnected()) // Wait until remaining messages are sent <-triggerC - assert.False(t, suite.clientDispatcher.IsPaused()) + suite.False(suite.clientDispatcher.IsPaused()) assert.True(t, suite.clientDispatcher.IsRunning()) assert.Equal(t, messagesToQueue, sentMessages) assert.True(t, suite.clientRequestQueue.IsEmpty()) - assert.False(t, state.HasPendingRequest()) + suite.False(state.HasPendingRequest()) assert.True(t, suite.chargePoint.IsConnected()) } @@ -782,7 +782,7 @@ func (suite *OcppJTestSuite) TestClientResponseTimeout() { // Wait for request to be enqueued, then check state time.Sleep(50 * time.Millisecond) state := suite.chargePoint.RequestState - assert.False(t, suite.clientRequestQueue.IsEmpty()) + suite.False(suite.clientRequestQueue.IsEmpty()) assert.True(t, suite.clientDispatcher.IsRunning()) assert.Equal(t, 1, suite.clientRequestQueue.Size()) assert.True(t, state.HasPendingRequest()) @@ -790,7 +790,7 @@ func (suite *OcppJTestSuite) TestClientResponseTimeout() { <-timeoutC assert.True(t, suite.clientRequestQueue.IsEmpty()) assert.True(t, suite.clientDispatcher.IsRunning()) - assert.False(t, state.HasPendingRequest()) + suite.False(state.HasPendingRequest()) } func (suite *OcppJTestSuite) TestStopDisconnectedClient() { @@ -812,15 +812,15 @@ func (suite *OcppJTestSuite) TestStopDisconnectedClient() { time.Sleep(100 * time.Millisecond) // Dispatcher should be paused assert.True(t, suite.clientDispatcher.IsPaused()) - assert.False(t, suite.chargePoint.IsConnected()) + suite.False(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()) + suite.False(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()) + suite.False(suite.chargePoint.IsConnected()) } diff --git a/ocppj/client.go b/ocppj/client.go index 720575f7..cfa004db 100644 --- a/ocppj/client.go +++ b/ocppj/client.go @@ -209,9 +209,10 @@ func (c *Client) SendRequest(request ocpp.Request) error { return nil } +// SendEvent s an OCPP SEND event to the server. func (c *Client) SendEvent(request ocpp.Request) error { if !c.dispatcher.IsRunning() { - return fmt.Errorf("ocppj client is not started, couldn't send request") + return fmt.Errorf("ocppj client is not started, couldn't send event") } // Check if the request feature is a Stream feature @@ -220,21 +221,23 @@ func (c *Client) SendEvent(request ocpp.Request) error { return fmt.Errorf("ocppj client can only send events for Stream features, got: %s", feature) } - call, err := c.CreateCall(request) + send, err := c.CreateSend(request) if err != nil { return err } - jsonMessage, err := call.MarshalJSON() + + jsonMessage, err := send.MarshalJSON() if err != nil { return err } + // Message will be processed by dispatcher. A dedicated mechanism allows to delegate the message queue handling. - if err = c.dispatcher.SendRequest(RequestBundle{Call: call, Data: jsonMessage}); err != nil { - log.Errorf("error dispatching request [%s, %s]: %v", call.UniqueId, call.Action, err) + if err = c.dispatcher.SendEvent(EventBundle{Send: send, Data: jsonMessage}); err != nil { + log.Errorf("error dispatching SEND [%s, %s]: %v", send.UniqueId, send.Action, err) return err } - log.Debugf("enqueued CALL [%s, %s]", call.UniqueId, call.Action) + log.Debugf("enqueued SEND [%s, %s]", send.UniqueId, send.Action) return nil } diff --git a/ocppj/dispatcher.go b/ocppj/dispatcher.go index f27cf6bd..8af55487 100644 --- a/ocppj/dispatcher.go +++ b/ocppj/dispatcher.go @@ -40,6 +40,12 @@ type ClientDispatcher interface { // // If no network client was set, or the request couldn't be processed, an error is returned. SendRequest(req RequestBundle) error + + // Dispatches a send event. Depending on the implementation, this may first queue a request + // and process it later, asynchronously, or write it directly to the networking layer. + // + // If no network client was set, or the request couldn't be processed, an error is returned. + SendEvent(req EventBundle) error // Notifies the dispatcher that a request has been completed (i.e. a response was received). // The dispatcher takes care of removing the request marked by the requestID from // the pending requests. It will then attempt to process the next queued request. @@ -170,6 +176,19 @@ func (d *DefaultClientDispatcher) SendRequest(req RequestBundle) error { return nil } +func (d *DefaultClientDispatcher) SendEvent(req EventBundle) error { + if d.network == nil { + return fmt.Errorf("cannot SendEvent, no network client was set") + } + if err := d.requestQueue.Push(req); err != nil { + return err + } + d.mutex.RLock() + d.requestChannel <- true + d.mutex.RUnlock() + return nil +} + func (d *DefaultClientDispatcher) messagePump() { rdy := true // Ready to transmit at the beginning @@ -319,6 +338,11 @@ type ServerDispatcher interface { // // If no network server was set, or the request couldn't be processed, an error is returned. SendRequest(clientID string, req RequestBundle) error + // Dispatches a SEND request for a specific client. Depending on the implementation, this may first queue + // a request and process it later (asynchronously), or write it directly to the networking layer. + // + // If no network server was set, or the request couldn't be processed, an error is returned. + SendEvent(clientID string, req EventBundle) error // Notifies the dispatcher that a request has been completed (i.e. a response was received), // for a specific client. // The dispatcher takes care of removing the request marked by the requestID from @@ -471,6 +495,23 @@ func (d *DefaultServerDispatcher) SendRequest(clientID string, req RequestBundle return nil } +func (d *DefaultServerDispatcher) SendEvent(clientID string, req EventBundle) error { + if d.network == nil { + return fmt.Errorf("cannot send event %v, no network server was set", req.Send.UniqueId) + } + q, ok := d.queueMap.Get(clientID) + if !ok { + return fmt.Errorf("cannot send event %s, no client %s exists", req.Send.UniqueId, clientID) + } + if err := q.Push(req); err != nil { + return err + } + d.mutex.RLock() + d.requestChannel <- clientID + d.mutex.RUnlock() + return nil +} + // requestPump processes new outgoing requests for each client and makes sure they are processed sequentially. // This method is executed by a dedicated coroutine as soon as the server is started and runs indefinitely. func (d *DefaultServerDispatcher) messagePump() { diff --git a/ocppj/dispatcher_test.go b/ocppj/dispatcher_test.go index a33d5162..43a9a945 100644 --- a/ocppj/dispatcher_test.go +++ b/ocppj/dispatcher_test.go @@ -285,6 +285,38 @@ func (c *ClientDispatcherTestSuite) TestClientSendRequest() { } +func (c *ClientDispatcherTestSuite) TestClientSendEvent() { + // Setup + sent := make(chan bool, 1) + c.websocketClient.On("Write", mock.Anything).Run(func(args mock.Arguments) { + sent <- true + }).Return(nil) + c.dispatcher.Start() + c.Require().True(c.dispatcher.IsRunning()) + + // Create and send mock request + req := newMockRequest("somevalue") + + call, err := c.endpoint.CreateSend(req) + c.Require().NoError(err) + + data, err := call.MarshalJSON() + c.Require().NoError(err) + + bundle := ocppj.RequestBundle{Call: (*ocppj.Call)(call), Data: data} + err = c.dispatcher.SendRequest(bundle) + c.Require().NoError(err) + + // Check underlying queue + c.False(c.queue.IsEmpty()) + c.Equal(1, c.queue.Size()) + + // Wait for websocket to send message + _, ok := <-sent + c.True(ok) + c.True(c.state.HasPendingRequest()) +} + func (c *ClientDispatcherTestSuite) TestClientRequestCanceled() { t := c.T() // Setup diff --git a/ocppj/ocppj.go b/ocppj/ocppj.go index 7da7a494..85b2d20a 100644 --- a/ocppj/ocppj.go +++ b/ocppj/ocppj.go @@ -187,7 +187,7 @@ func (callError *CallError) MarshalJSON() ([]byte, error) { // An OCPP-J CallResultError message, containing an OCPP Result Error. type CallResultError struct { Message - MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=4"` + MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=5"` UniqueId string `json:"uniqueId" validate:"required,max=36"` ErrorCode ocpp.ErrorCode `json:"errorCode" validate:"errorCode"` ErrorDescription string `json:"errorDescription" validate:"omitempty,max=255"` @@ -221,7 +221,7 @@ func (callError *CallResultError) MarshalJSON() ([]byte, error) { // An OCPP-J SEND message, containing an OCPP Request. type Send struct { Message `validate:"-"` - MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=2"` + MessageTypeId MessageType `json:"messageTypeId" validate:"required,eq=6"` UniqueId string `json:"uniqueId" validate:"required,max=36"` Action string `json:"action" validate:"required,max=36"` Payload ocpp.Request `json:"payload" validate:"required"` @@ -485,7 +485,7 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl return nil, errorFromValidation(endpoint, err.(validator.ValidationErrors), uniqueId, request.GetFeatureName()) } return &callResult, nil - case CALL_ERROR, CALL_RESULT_ERROR: + case CALL_ERROR: _, ok := pendingRequestState.GetPendingRequest(uniqueId) if !ok { log.Infof("No previous request %v sent. Discarding error message", uniqueId) @@ -519,15 +519,80 @@ func (endpoint *Endpoint) ParseMessage(arr []interface{}, pendingRequestState Cl return nil, errorFromValidation(endpoint, err.(validator.ValidationErrors), uniqueId, "") } return &callError, nil + case CALL_RESULT_ERROR: + // Onl + if endpoint.Dialect() != ocpp.V21 { + return nil, ocpp.NewError(MessageTypeNotSupported, "CALL_RESULT_ERROR message is not supported in this OCPP version", uniqueId) + } + + _, ok := pendingRequestState.GetPendingRequest(uniqueId) + if !ok { + log.Infof("No previous request %v sent. Discarding error message", uniqueId) + return nil, nil + } + + if len(arr) < 4 { + return nil, ocpp.NewError(FormatErrorType(endpoint), "Invalid Call Error message. Expected array length >= 4", uniqueId) + } + var details interface{} + if len(arr) > 4 { + details = arr[4] + } + + rawErrorCode, ok := arr[2].(string) + if !ok { + return nil, ocpp.NewError(FormatErrorType(endpoint), 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 { + errorDescription = v + } + + callResultError := CallResultError{ + MessageTypeId: CALL_ERROR, + UniqueId: uniqueId, + ErrorCode: errorCode, + ErrorDescription: errorDescription, + ErrorDetails: details, + } + err := Validate.Struct(callResultError) + if err != nil { + return nil, errorFromValidation(endpoint, err.(validator.ValidationErrors), uniqueId, "") + } + return &callResultError, nil case SEND: // SEND can be only sent in OCPP 2.1 if endpoint.Dialect() != ocpp.V21 { return nil, ocpp.NewError(MessageTypeNotSupported, "SEND message is not supported in this OCPP version", uniqueId) } - // Send does not expect a confirmation, so it is not added to the pending requests. - return nil, nil + if len(arr) != 4 { + return nil, ocpp.NewError(FormatErrorType(endpoint), "Invalid Call message. Expected array length 4", uniqueId) + } + action, ok := arr[2].(string) + if !ok { + return nil, ocpp.NewError(FormatErrorType(endpoint), fmt.Sprintf("Invalid element %v at 2, expected action (string)", arr[2]), uniqueId) + } + + profile, ok := endpoint.GetProfileForFeature(action) + if !ok { + return nil, ocpp.NewError(NotSupported, fmt.Sprintf("Unsupported feature %v", action), uniqueId) + } + + request, err := profile.ParseRequest(action, arr[3], parseRawJsonRequest) + if err != nil { + return nil, err + } + + return &Send{ + MessageTypeId: SEND, + UniqueId: uniqueId, + Action: action, + Payload: request, + }, nil default: return nil, ocpp.NewError(MessageTypeNotSupported, fmt.Sprintf("Invalid message type ID %v", typeId), uniqueId) } @@ -568,19 +633,19 @@ func (endpoint *Endpoint) CreateSend(request ocpp.Request) (*Send, error) { } // TODO: handle collisions? uniqueId := messageIdGenerator() - call := Send{ + send := Send{ MessageTypeId: SEND, UniqueId: uniqueId, Action: action, Payload: request, } if validationEnabled { - err := Validate.Struct(call) + err := Validate.Struct(send) if err != nil { return nil, err } } - return &call, nil + return &send, nil } // Creates a CallResultError message, given the message's unique ID and the error. diff --git a/ocppj/ocppj_test.go b/ocppj/ocppj_test.go index f6d16268..e9f99a94 100644 --- a/ocppj/ocppj_test.go +++ b/ocppj/ocppj_test.go @@ -225,6 +225,14 @@ func newMockConfirmation(value string) *MockConfirmation { return &MockConfirmation{MockValue: value} } +type MockStream struct { + MockValue string `json:"mockValue" validate:"required,min=5"` +} + +func (m *MockStream) GetFeatureName() string { + return "MockStream" +} + type MockUnsupportedResponse struct { MockValue string `json:"mockValue" validate:"required,min=5"` } @@ -233,43 +241,6 @@ func (m *MockUnsupportedResponse) GetFeatureName() string { return "SomeRandomFeature" } -// ---------------------- COMMON UTILITY METHODS ---------------------- - -func NewWebsocketServer(t *testing.T, onMessage func(data []byte) ([]byte, error)) ws.Server { - wsServer := ws.NewServer() - wsServer.SetMessageHandler(func(ws ws.Channel, data []byte) error { - assert.NotNil(t, ws) - assert.NotNil(t, data) - if onMessage != nil { - response, err := onMessage(data) - assert.Nil(t, err) - if response != nil { - err = wsServer.Write(ws.ID(), data) - assert.Nil(t, err) - } - } - return nil - }) - return wsServer -} - -func NewWebsocketClient(t *testing.T, onMessage func(data []byte) ([]byte, error)) ws.Client { - wsClient := ws.NewClient() - wsClient.SetMessageHandler(func(data []byte) error { - assert.NotNil(t, data) - if onMessage != nil { - response, err := onMessage(data) - assert.Nil(t, err) - if response != nil { - err = wsClient.Write(data) - assert.Nil(t, err) - } - } - return nil - }) - return wsClient -} - func ParseCall(endpoint *ocppj.Endpoint, state ocppj.ClientState, json string, t *testing.T) *ocppj.Call { parsedData, err := ocppj.ParseJsonMessage(json) require.NoError(t, err) @@ -520,16 +491,16 @@ func (suite *OcppJTestSuite) TestCreateSend() { mockValue := "somevalue" request := newMockRequest(mockValue) call, err := suite.chargePoint.CreateSend(request) - assert.Nil(t, err) - CheckSend(call, t, MockFeatureName+"Stream", call.UniqueId) + suite.NoError(err) + CheckSend(call, t, MockFeatureName, call.UniqueId) message, ok := call.Payload.(*MockRequest) assert.True(t, ok) assert.NotNil(t, message) assert.Equal(t, mockValue, message.MockValue) // Check that request was not yet stored as pending request pendingRequest, exists := suite.chargePoint.RequestState.GetPendingRequest(call.UniqueId) - assert.False(t, exists) - assert.Nil(t, pendingRequest) + suite.False(exists) + suite.Nil(t, pendingRequest) } func (suite *OcppJTestSuite) TestCreateCallResult() { diff --git a/ocppj/queue.go b/ocppj/queue.go index c1f3eeb1..fdf57033 100644 --- a/ocppj/queue.go +++ b/ocppj/queue.go @@ -12,6 +12,13 @@ type RequestBundle struct { Data []byte } +// EventBundle is a convenience struct for passing a send object struct and the +// raw byte data into the queue containing outgoing requests. +type EventBundle struct { + Send *Send + Data []byte +} + // RequestQueue can be arbitrarily implemented, as long as it conforms to the Queue interface. // // A RequestQueue is used by ocppj client and server to manage outgoing requests. diff --git a/ocppj/queue_test.go b/ocppj/queue_test.go index c12f9853..7ba53a35 100644 --- a/ocppj/queue_test.go +++ b/ocppj/queue_test.go @@ -2,8 +2,6 @@ package ocppj_test import ( "github.com/lorenzodonini/ocpp-go/ocppj" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -19,104 +17,96 @@ func (suite *ClientQueueTestSuite) SetupTest() { } func (suite *ClientQueueTestSuite) TestQueueEmpty() { - t := suite.T() empty := suite.queue.IsEmpty() - assert.True(t, empty) + suite.True(empty) } func (suite *ClientQueueTestSuite) TestPushElement() { - t := suite.T() req := newMockRequest("somevalue") err := suite.queue.Push(req) - require.Nil(t, err) - assert.False(t, suite.queue.IsEmpty()) - assert.False(t, suite.queue.IsFull()) - assert.Equal(t, 1, suite.queue.Size()) + suite.Require().NoError(err) + suite.False(suite.queue.IsEmpty()) + suite.False(suite.queue.IsFull()) + suite.Equal(1, suite.queue.Size()) } func (suite *ClientQueueTestSuite) TestQueueSize() { - t := suite.T() for i := 0; i < queueCapacity; i++ { req := newMockRequest("somevalue") err := suite.queue.Push(req) - require.Nil(t, err) - assert.False(t, suite.queue.IsEmpty()) - assert.Equal(t, i+1, suite.queue.Size()) + suite.Require().NoError(err) + suite.False(suite.queue.IsEmpty()) + suite.Equal(i+1, suite.queue.Size()) } } func (suite *ClientQueueTestSuite) TestQueueFull() { - t := suite.T() for i := 0; i < queueCapacity+2; i++ { req := newMockRequest("somevalue") err := suite.queue.Push(req) if i < queueCapacity { - require.Nil(t, err) + suite.Require().Nil(err) if i < queueCapacity-1 { - assert.False(t, suite.queue.IsFull()) + suite.False(suite.queue.IsFull()) } else { - assert.True(t, suite.queue.IsFull()) + suite.True(suite.queue.IsFull()) } } else { - require.NotNil(t, err) - assert.True(t, suite.queue.IsFull()) + suite.Require().NoError(err) + suite.True(suite.queue.IsFull()) } } } func (suite *ClientQueueTestSuite) TestPeekElement() { - t := suite.T() req := newMockRequest("somevalue") err := suite.queue.Push(req) - require.Nil(t, err) + suite.Require().NoError(err) el := suite.queue.Peek() - require.NotNil(t, el) + suite.Require().NotNil(el) peeked, ok := el.(*MockRequest) - require.True(t, ok) - require.NotNil(t, peeked) - assert.Equal(t, req.MockValue, peeked.MockValue) - assert.False(t, suite.queue.IsEmpty()) - assert.False(t, suite.queue.IsFull()) - assert.Equal(t, 1, suite.queue.Size()) + suite.Require().True(ok) + suite.Require().NotNil(peeked) + suite.Equal(req.MockValue, peeked.MockValue) + suite.False(suite.queue.IsEmpty()) + suite.False(suite.queue.IsFull()) + suite.Equal(1, suite.queue.Size()) } func (suite *ClientQueueTestSuite) TestPopElement() { - t := suite.T() req := newMockRequest("somevalue") err := suite.queue.Push(req) - require.Nil(t, err) + suite.Require().Nil(err) el := suite.queue.Pop() - require.NotNil(t, el) + suite.Require().NotNil(el) popped, ok := el.(*MockRequest) - require.True(t, ok) - require.NotNil(t, popped) - assert.Equal(t, req.MockValue, popped.MockValue) - assert.True(t, suite.queue.IsEmpty()) - assert.False(t, suite.queue.IsFull()) + suite.Require().True(ok) + suite.Require().NotNil(popped) + suite.Equal(req.MockValue, popped.MockValue) + suite.True(suite.queue.IsEmpty()) + suite.False(suite.queue.IsFull()) } func (suite *ClientQueueTestSuite) TestQueueNoCapacity() { - t := suite.T() suite.queue = ocppj.NewFIFOClientQueue(0) for i := 0; i < 50; i++ { req := newMockRequest("somevalue") err := suite.queue.Push(req) - require.Nil(t, err) + suite.Require().NoError(err) } - assert.False(t, suite.queue.IsFull()) + suite.False(suite.queue.IsFull()) } func (suite *ClientQueueTestSuite) TestQueueClear() { - t := suite.T() for i := 0; i < queueCapacity; i++ { req := newMockRequest("somevalue") err := suite.queue.Push(req) - require.Nil(t, err) + suite.Require().NoError(err) } - assert.True(t, suite.queue.IsFull()) + suite.True(suite.queue.IsFull()) suite.queue.Init() - assert.True(t, suite.queue.IsEmpty()) - assert.Equal(t, 0, suite.queue.Size()) + suite.True(suite.queue.IsEmpty()) + suite.Equal(0, suite.queue.Size()) } type ServerQueueMapTestSuite struct { @@ -129,7 +119,6 @@ func (suite *ServerQueueMapTestSuite) SetupTest() { } func (suite *ServerQueueMapTestSuite) TestAddElement() { - t := suite.T() q := ocppj.NewFIFOClientQueue(0) el := "element1" _ = q.Push(el) @@ -137,37 +126,35 @@ func (suite *ServerQueueMapTestSuite) TestAddElement() { suite.queueMap.Add(id, q) retrieved, ok := suite.queueMap.Get(id) - require.True(t, ok) - require.NotNil(t, retrieved) - assert.False(t, retrieved.IsEmpty()) - assert.Equal(t, 1, retrieved.Size()) - assert.Equal(t, el, retrieved.Peek()) + suite.Require().True(ok) + suite.Require().NotNil(retrieved) + suite.False(retrieved.IsEmpty()) + suite.Equal(1, retrieved.Size()) + suite.Equal(el, retrieved.Peek()) } func (suite *ServerQueueMapTestSuite) TestGetOrCreate() { - t := suite.T() el := "element1" id := "test" q, ok := suite.queueMap.Get(id) - require.False(t, ok) - require.Nil(t, q) + suite.Require().False(ok) + suite.Require().Nil(q) q = suite.queueMap.GetOrCreate(id) - require.NotNil(t, q) + suite.Require().NotNil(q) _ = q.Push(el) // Verify consistency q, ok = suite.queueMap.Get(id) - require.True(t, ok) - assert.Equal(t, 1, q.Size()) - assert.Equal(t, el, q.Peek()) + suite.Require().True(ok) + suite.Equal(1, q.Size()) + suite.Equal(el, q.Peek()) } func (suite *ServerQueueMapTestSuite) TestRemove() { - t := suite.T() id := "test" q := suite.queueMap.GetOrCreate(id) - require.NotNil(t, q) + suite.Require().NotNil(q) suite.queueMap.Remove(id) q, ok := suite.queueMap.Get(id) - assert.False(t, ok) - assert.Nil(t, q) + suite.False(ok) + suite.Nil(q) } diff --git a/ocppj/server.go b/ocppj/server.go index 2b0365da..88d61ede 100644 --- a/ocppj/server.go +++ b/ocppj/server.go @@ -286,6 +286,19 @@ func (s *Server) ocppMessageHandler(wsChannel ws.Channel, data []byte) error { if s.errorHandler != nil { s.errorHandler(wsChannel, ocpp.NewError(callError.ErrorCode, callError.ErrorDescription, callError.UniqueId), callError.ErrorDetails) } + case CALL_RESULT_ERROR: + callResultError := message.(*CallResultError) + log.Debugf("handling incoming CALL RESULT ERROR [%s] from %s", callResultError.UniqueId, wsChannel.ID()) + // Nothing to complete s.dispatcher.CompleteRequest(wsChannel.ID(), callResultError.GetUniqueId()) + if s.errorHandler != nil { + s.errorHandler(wsChannel, ocpp.NewError(callResultError.ErrorCode, callResultError.ErrorDescription, callResultError.UniqueId), callResultError.ErrorDetails) + } + case SEND: + send := message.(*Send) + log.Debugf("handling incoming SEND [%s, %s] from %s", send.UniqueId, send.Action, wsChannel.ID()) + if s.requestHandler != nil { + s.requestHandler(wsChannel, send.Payload, send.UniqueId, send.Action) + } } } return nil @@ -315,6 +328,7 @@ func (s *Server) HandleFailedResponseError(clientID string, requestID string, er // 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) } diff --git a/ocppj/state_test.go b/ocppj/state_test.go index 8f98ffa2..82e0eac0 100644 --- a/ocppj/state_test.go +++ b/ocppj/state_test.go @@ -4,7 +4,6 @@ import ( "sync" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/lorenzodonini/ocpp-go/ocppj" @@ -20,33 +19,30 @@ func (suite *ClientStateTestSuite) SetupTest() { } func (suite *ClientStateTestSuite) TestAddPendingRequest() { - t := suite.T() requestID := "1234" req := newMockRequest("somevalue") - require.False(t, suite.state.HasPendingRequest()) + suite.Require().False(suite.state.HasPendingRequest()) suite.state.AddPendingRequest(requestID, req) - require.True(t, suite.state.HasPendingRequest()) + suite.Require().True(suite.state.HasPendingRequest()) r, exists := suite.state.GetPendingRequest(requestID) - assert.True(t, exists) - assert.Equal(t, req, r) + suite.True(exists) + suite.Equal(req, r) } func (suite *ClientStateTestSuite) TestGetInvalidPendingRequest() { - t := suite.T() requestID := "1234" suite.state.AddPendingRequest(requestID, newMockRequest("somevalue")) - require.True(t, suite.state.HasPendingRequest()) + suite.Require().True(suite.state.HasPendingRequest()) invalidRequestIDs := []string{"4321", "5678", "1230", "deadc0de"} // Nothing returned when querying for an unknown request ID for _, id := range invalidRequestIDs { r, exists := suite.state.GetPendingRequest(id) - assert.False(t, exists) - assert.Nil(t, r) + suite.False(exists) + suite.Nil(r) } } func (suite *ClientStateTestSuite) TestAddMultiplePendingRequests() { - t := suite.T() requestId1 := "1234" requestId2 := "5678" req1 := newMockRequest("somevalue1") @@ -54,53 +50,50 @@ func (suite *ClientStateTestSuite) TestAddMultiplePendingRequests() { suite.state.AddPendingRequest(requestId1, req1) suite.state.AddPendingRequest(requestId2, req2) r, exists := suite.state.GetPendingRequest(requestId1) - assert.True(t, exists) - assert.NotNil(t, r) + suite.True(exists) + suite.NotNil(r) r, exists = suite.state.GetPendingRequest(requestId2) - assert.False(t, exists) - assert.Nil(t, r) + suite.False(exists) + suite.Nil(r) } func (suite *ClientStateTestSuite) TestDeletePendingRequest() { - t := suite.T() requestID := "1234" req := newMockRequest("somevalue") suite.state.AddPendingRequest(requestID, req) - require.True(t, suite.state.HasPendingRequest()) + suite.Require().True(suite.state.HasPendingRequest()) suite.state.DeletePendingRequest(requestID) // Previously added request is gone - assert.False(t, suite.state.HasPendingRequest()) + suite.False(suite.state.HasPendingRequest()) r, exists := suite.state.GetPendingRequest(requestID) - assert.False(t, exists) - assert.Nil(t, r) + suite.False(exists) + suite.Nil(r) // Deleting again has no effect suite.state.DeletePendingRequest(requestID) - assert.False(t, suite.state.HasPendingRequest()) + suite.False(suite.state.HasPendingRequest()) } func (suite *ClientStateTestSuite) TestDeleteInvalidPendingRequest() { - t := suite.T() requestID := "1234" req := newMockRequest("somevalue") suite.state.AddPendingRequest(requestID, req) - require.True(t, suite.state.HasPendingRequest()) + suite.Require().True(suite.state.HasPendingRequest()) suite.state.DeletePendingRequest("5678") // Previously added request is still there - assert.True(t, suite.state.HasPendingRequest()) + suite.True(suite.state.HasPendingRequest()) r, exists := suite.state.GetPendingRequest(requestID) - assert.True(t, exists) - assert.NotNil(t, r) + suite.True(exists) + suite.NotNil(r) } func (suite *ClientStateTestSuite) TestClearPendingRequests() { - t := suite.T() requestID := "1234" req := newMockRequest("somevalue") suite.state.AddPendingRequest(requestID, req) - require.True(t, suite.state.HasPendingRequest()) + suite.Require().True(suite.state.HasPendingRequest()) suite.state.ClearPendingRequests() // No more requests available in the struct - assert.False(t, suite.state.HasPendingRequest()) + suite.False(suite.state.HasPendingRequest()) } type ServerStateTestSuite struct { @@ -125,94 +118,91 @@ func (suite *ServerStateTestSuite) TestAddPendingRequests() { {"client2", "0002", newMockRequest("somevalue2")}, {"client3", "0003", newMockRequest("somevalue3")}, } + for _, r := range requests { suite.state.AddPendingRequest(r.clientID, r.requestID, r.request) } - require.True(t, suite.state.HasPendingRequests()) + suite.Require().True(suite.state.HasPendingRequests()) + for _, r := range requests { - assert.True(t, suite.state.HasPendingRequest(r.clientID)) + suite.True(suite.state.HasPendingRequest(r.clientID)) req, exists := suite.state.GetClientState(r.clientID).GetPendingRequest(r.requestID) - assert.True(t, exists) + suite.True(exists) assert.Equal(t, r.request, req) } } func (suite *ServerStateTestSuite) TestGetInvalidPendingRequest() { - t := suite.T() requestID := "1234" clientID := "client1" suite.state.AddPendingRequest(clientID, requestID, newMockRequest("somevalue")) - require.True(t, suite.state.HasPendingRequest(clientID)) + suite.Require().True(suite.state.HasPendingRequest(clientID)) invalidRequestIDs := []string{"4321", "5678", "1230", "deadc0de"} // Nothing returned when querying for an unknown request ID for _, id := range invalidRequestIDs { r, exists := suite.state.GetClientState(clientID).GetPendingRequest(id) - assert.False(t, exists) - assert.Nil(t, r) + suite.False(exists) + suite.Nil(r) } } func (suite *ServerStateTestSuite) TestClearClientPendingRequests() { - t := suite.T() client1 := "client1" client2 := "client2" suite.state.AddPendingRequest(client1, "1234", newMockRequest("somevalue1")) suite.state.AddPendingRequest(client2, "5678", newMockRequest("somevalue2")) - require.True(t, suite.state.HasPendingRequest(client1)) + suite.Require().True(suite.state.HasPendingRequest(client1)) suite.state.ClearClientPendingRequest(client1) // Request for client1 is deleted - assert.False(t, suite.state.HasPendingRequest(client1)) + suite.False(suite.state.HasPendingRequest(client1)) r, exists := suite.state.GetClientState(client1).GetPendingRequest("1234") - assert.False(t, exists) - assert.Nil(t, r) + suite.False(exists) + suite.Nil(r) // Request for client2 is safe and sound - assert.True(t, suite.state.HasPendingRequest(client2)) + suite.True(suite.state.HasPendingRequest(client2)) } func (suite *ServerStateTestSuite) TestClearAllPendingRequests() { - t := suite.T() client1 := "client1" client2 := "client2" suite.state.AddPendingRequest(client1, "1234", newMockRequest("somevalue1")) suite.state.AddPendingRequest(client2, "5678", newMockRequest("somevalue2")) - require.True(t, suite.state.HasPendingRequests()) + suite.Require().True(suite.state.HasPendingRequests()) suite.state.ClearAllPendingRequests() - assert.False(t, suite.state.HasPendingRequests()) + suite.False(suite.state.HasPendingRequests()) // No more requests available in the struct - assert.False(t, suite.state.HasPendingRequest(client1)) - assert.False(t, suite.state.HasPendingRequest(client2)) + suite.False(suite.state.HasPendingRequest(client1)) + suite.False(suite.state.HasPendingRequest(client2)) } func (suite *ServerStateTestSuite) TestDeletePendingRequest() { - t := suite.T() client1 := "client1" client2 := "client2" suite.state.AddPendingRequest(client1, "1234", newMockRequest("somevalue1")) suite.state.AddPendingRequest(client2, "5678", newMockRequest("somevalue2")) - require.True(t, suite.state.HasPendingRequest(client1)) - require.True(t, suite.state.HasPendingRequest(client2)) + suite.Require().True(suite.state.HasPendingRequest(client1)) + suite.Require().True(suite.state.HasPendingRequest(client2)) suite.state.DeletePendingRequest(client1, "1234") // Previously added request for client1 is gone - assert.False(t, suite.state.HasPendingRequest(client1)) + suite.False(suite.state.HasPendingRequest(client1)) r, exists := suite.state.GetClientState(client1).GetPendingRequest("1234") - assert.False(t, exists) - assert.Nil(t, r) + suite.False(exists) + suite.Nil(r) // Deleting again has no effect suite.state.DeletePendingRequest(client1, "1234") - assert.False(t, suite.state.HasPendingRequest(client1)) + suite.False(suite.state.HasPendingRequest(client1)) // Previously added request for client2 is unaffected - assert.True(t, suite.state.HasPendingRequest(client2)) + suite.True(suite.state.HasPendingRequest(client2)) } func (suite *ServerStateTestSuite) TestDeleteInvalidPendingRequest() { - t := suite.T() client1 := "client1" suite.state.AddPendingRequest(client1, "1234", newMockRequest("somevalue1")) - require.True(t, suite.state.HasPendingRequest(client1)) + suite.Require().True(suite.state.HasPendingRequest(client1)) suite.state.DeletePendingRequest(client1, "5678") // Previously added request is still there - assert.True(t, suite.state.HasPendingRequest(client1)) + suite.True(suite.state.HasPendingRequest(client1)) r, exists := suite.state.GetClientState(client1).GetPendingRequest("1234") - assert.True(t, exists) - assert.NotNil(t, r) + suite.True(exists) + suite.NotNil(r) } From 775710e5adbaddd1e7b9d06a2b2008d353d04a1b Mon Sep 17 00:00:00 2001 From: xBlaz3kx Date: Sat, 16 Aug 2025 10:08:26 +0200 Subject: [PATCH 12/12] fix: incorrect datetime reference --- ocpp2.1/types/cost.go | 3 +-- ocpp2.1/types/smart_charging.go | 3 +-- ocpp2.1/types/tariff.go | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ocpp2.1/types/cost.go b/ocpp2.1/types/cost.go index a88b3957..25bd6a2b 100644 --- a/ocpp2.1/types/cost.go +++ b/ocpp2.1/types/cost.go @@ -1,7 +1,6 @@ package types import ( - "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" "gopkg.in/go-playground/validator.v9" ) @@ -151,7 +150,7 @@ type TotalUsage struct { type ChargingPeriod struct { TariffId *string `json:"tariffId,omitempty" validate:"omitempty,max=60"` // The ID of the tariff used for this charging period. - StartPeriod types.DateTime `json:"startPeriod" validate:"required"` // The start of the charging period. + StartPeriod DateTime `json:"startPeriod" validate:"required"` // The start of the charging period. Dimensions []CostDimension `json:"dimensions,omitempty" validate:"omitempty,dive"` } diff --git a/ocpp2.1/types/smart_charging.go b/ocpp2.1/types/smart_charging.go index cfa6f504..823c6609 100644 --- a/ocpp2.1/types/smart_charging.go +++ b/ocpp2.1/types/smart_charging.go @@ -1,7 +1,6 @@ package types import ( - "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" "gopkg.in/go-playground/validator.v9" ) @@ -174,7 +173,7 @@ type ChargingProfile struct { MaxOfflineDuration *int `json:"maxOfflineDuration,omitempty" validate:"omitempty"` InvalidAfterOfflineDuration bool `json:"invalidAfterOfflineDuration,omitempty" validate:"omitempty"` DynUpdateInterval *int `json:"dynUpdateInterval,omitempty" validate:"omitempty"` - DynUpdateTime *types.DateTime `json:"dynUpdateTime,omitempty" validate:"omitempty"` + DynUpdateTime *DateTime `json:"dynUpdateTime,omitempty" validate:"omitempty"` PriceScheduleSignature *string `json:"priceScheduleSignature,omitempty" validate:"omitempty,max=256"` ChargingSchedule []ChargingSchedule `json:"chargingSchedule" validate:"required,min=1,max=3,dive"` } diff --git a/ocpp2.1/types/tariff.go b/ocpp2.1/types/tariff.go index c24a2431..5c54e177 100644 --- a/ocpp2.1/types/tariff.go +++ b/ocpp2.1/types/tariff.go @@ -1,7 +1,6 @@ package types import ( - "github.com/lorenzodonini/ocpp-go/ocpp2.0.1/types" "github.com/lorenzodonini/ocpp-go/ocppj" "gopkg.in/go-playground/validator.v9" ) @@ -9,7 +8,7 @@ import ( type Tariff struct { TariffId string `json:"tariffId" validate:"required,max=60"` // Identifier used to identify one tariff. Currency string `json:"currency" validate:"required,max=3"` - ValidFrom *types.DateTime `json:"validFrom,omitempty" validate:"omitempty"` + ValidFrom *DateTime `json:"validFrom,omitempty" validate:"omitempty"` Description []MessageContent `json:"description,omitempty" validate:"omitempty,max=10,dive"` Energy *TariffEnergy `json:"energy,omitempty" validate:"omitempty,dive"` ChargingTime *TariffTime `json:"chargingTime,omitempty" validate:"omitempty,dive"`