π Your Complete Guide to OCPP 1.6 WebSocket Implementation
Master every event, every key, and every detail for perfect EV charging communication
π OCPP Server (Central System)
βοΈ WebSocket Connection
π Charge Point (EV Charger)
WebSocket URL Format:
ws://server:port/ocpp/CP001
wss://server:port/ocpp/CP001 (Secure)
- π― Central System Initiated Events
- π Reset
- π UnlockConnector
- π RemoteStartTransaction
- π΄ RemoteStopTransaction
- βοΈ ChangeConfiguration
- π GetConfiguration
- π GetDiagnostics
- π UpdateFirmware
- π ReserveNow
- β CancelReservation
- ποΈ ClearCache
- π GetLocalListVersion
- π€ SendLocalList
- β‘ SetChargingProfile
- ποΈ ClearChargingProfile
- π GetCompositeSchedule
- π― TriggerMessage
- π§ ChangeAvailability
All OCPP messages follow this JSON array format:
[MessageType, MessageId, Action, Payload]Message Types:
2= CALL (Request)3= CALLRESULT (Response)4= CALLERROR (Error)
sequenceDiagram
participant CP as Charge Point
participant CS as Central System
CP->>CS: [2, "msg_001", "Heartbeat", {}]
CS->>CP: [3, "msg_001", {"currentTime": "2024-01-01T12:00:00.000Z"}]
- Plain WebSocket connection
- No authentication required
β οΈ Not recommended for production
- HTTP Basic Authentication
- Username/Password over HTTPS
- TLS 1.2+ required
- Mutual TLS (mTLS)
- Client certificate authentication
- β Recommended for production
- Client certificate + HTTP Basic Auth
- Enhanced security logging
- π Maximum security
π Events sent from the Charge Point to the Central System
Purpose: Keep connection alive and sync time
π€ Request:
[2, "heartbeat_001", "Heartbeat", {}]π₯ Response:
[
3,
"heartbeat_001",
{
"currentTime": "2024-01-01T12:00:00.000Z"
}
]π Key Fields:
currentTime(DateTime): Server's current time in ISO 8601 format (Required - Response)
π Frequency: Every 30-300 seconds (configurable)
Purpose: Charge point registration and capability announcement
π€ Request:
[
2,
"boot_001",
"BootNotification",
{
"chargePointVendor": "Tesla",
"chargePointModel": "Supercharger V3",
"chargePointSerialNumber": "SC001234",
"chargeBoxSerialNumber": "CB001234",
"firmwareVersion": "1.8.2",
"iccid": "8944501234567890123",
"imsi": "234567890123456",
"meterType": "Carlo Gavazzi EM340",
"meterSerialNumber": "EM340001"
}
]π₯ Response:
[
3,
"boot_001",
{
"currentTime": "2024-01-01T12:00:00.000Z",
"interval": 300,
"status": "Accepted"
}
]π Key Fields:
chargePointVendor(String, Max 20): Vendor name (Required)chargePointModel(String, Max 20): Model name (Required)chargePointSerialNumber(String, Max 25): Serial number (optional)chargeBoxSerialNumber(String, Max 25): Charge box serial (optional)firmwareVersion(String, Max 50): Firmware version (optional)iccid(String, Max 20): SIM card identifier (optional)imsi(String, Max 20): Mobile subscriber identity (optional)meterType(String, Max 25): Energy meter type (optional)meterSerialNumber(String, Max 25): Meter serial number (optional)currentTime(DateTime): Server time (Required)interval(Integer): Heartbeat interval in seconds (Required)status(Enum): "Accepted", "Pending", "Rejected" (Required)
Example 1: Pending Registration:
[
3,
"boot_001",
{
"currentTime": "2024-01-01T12:00:00.000Z",
"interval": 300,
"status": "Pending"
}
]Example 2: Rejected Registration:
[
3,
"boot_002",
{
"currentTime": "2024-01-01T12:00:00.000Z",
"interval": 86400,
"status": "Rejected"
}
]Note: When status is "Pending", the charge point should retry BootNotification periodically. When "Rejected", the charge point should not attempt normal operations.
Purpose: Report connector status changes
π€ Request:
[
2,
"status_001",
"StatusNotification",
{
"connectorId": 1,
"status": "Occupied",
"errorCode": "NoError",
"info": "EV connected, charging in progress",
"timestamp": "2024-01-01T12:00:00.000Z",
"vendorId": "Tesla",
"vendorErrorCode": "T001"
}
]π₯ Response:
[3, "status_001", {}]π Key Fields:
connectorId(Integer): Connector ID (0 = charge point) (Required)status(Enum): "Available", "Preparing", "Charging", "SuspendedEVSE", "SuspendedEV", "Finishing", "Reserved", "Unavailable", "Faulted" (Required)errorCode(Enum): "ConnectorLockFailure", "EVCommunicationError", "GroundFailure", "HighTemperature", "InternalError", "LocalListConflict", "NoError", "OtherError", "OverCurrentFailure", "PowerMeterFailure", "PowerSwitchFailure", "ReaderFailure", "ResetFailure", "UnderVoltage", "OverVoltage", "WeakSignal" (Required)info(String, Max 50): Additional information (optional)timestamp(DateTime): Status change timestamp (optional)vendorId(String, Max 255): Vendor identifier (optional)vendorErrorCode(String, Max 50): Vendor-specific error code (optional)
Example 1: Fault Condition:
[
2,
"status_fault_001",
"StatusNotification",
{
"connectorId": 1,
"status": "Faulted",
"errorCode": "GroundFailure",
"info": "Ground fault detected on connector 1",
"timestamp": "2024-01-01T12:05:00.000Z",
"vendorId": "EVSE-Manufacturer",
"vendorErrorCode": "GF001"
}
]Example 2: Connector Unavailable:
[
2,
"status_unavail_001",
"StatusNotification",
{
"connectorId": 2,
"status": "Unavailable",
"errorCode": "PowerSwitchFailure",
"info": "Contactor failure detected",
"timestamp": "2024-01-01T12:10:00.000Z"
}
]Example 3: Charge Point Status (connectorId = 0):
[
2,
"status_cp_001",
"StatusNotification",
{
"connectorId": 0,
"status": "Available",
"errorCode": "NoError",
"timestamp": "2024-01-01T12:00:00.000Z"
}
]Purpose: Start a charging session
π€ Request:
[
2,
"start_001",
"StartTransaction",
{
"connectorId": 1,
"idTag": "RFID12345678",
"meterStart": 1000,
"timestamp": "2024-01-01T12:00:00.000Z",
"reservationId": 123
}
]π₯ Response:
[
3,
"start_001",
{
"transactionId": 98765,
"idTagInfo": {
"status": "Accepted",
"expiryDate": "2024-12-31T23:59:59.000Z",
"parentIdTag": "MASTER001"
}
}
]π Key Fields:
connectorId(Integer): Connector identifier (Required)idTag(String, Max 20): Authorization identifier (Required)meterStart(Integer): Energy meter value at start (Wh) (Required)timestamp(DateTime): Transaction start time (Required)reservationId(Integer): Reservation ID (optional)transactionId(Integer): Unique transaction identifier (Required - Response)idTagInfo(Object): Authorization information (Required - Response)status(Enum): "Accepted", "Blocked", "Expired", "Invalid", "ConcurrentTx" (Required)expiryDate(DateTime): Tag expiry date (optional)parentIdTag(String, Max 20): Parent tag identifier (optional)
Purpose: End a charging session
π€ Request:
[
2,
"stop_001",
"StopTransaction",
{
"transactionId": 98765,
"idTag": "RFID12345678",
"meterStop": 15000,
"timestamp": "2024-01-01T14:30:00.000Z",
"reason": "EVDisconnected",
"transactionData": [
{
"timestamp": "2024-01-01T12:30:00.000Z",
"sampledValue": [
{
"value": "5000",
"context": "Transaction.Begin",
"format": "Raw",
"measurand": "Energy.Active.Import.Register",
"location": "Outlet",
"unit": "Wh"
}
]
},
{
"timestamp": "2024-01-01T13:00:00.000Z",
"sampledValue": [
{
"value": "10000",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Energy.Active.Import.Register",
"location": "Outlet",
"unit": "Wh"
},
{
"value": "22.5",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Power.Active.Import",
"location": "Outlet",
"unit": "kW"
},
{
"value": "45",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "SoC",
"location": "EV",
"unit": "Percent"
}
]
},
{
"timestamp": "2024-01-01T14:30:00.000Z",
"sampledValue": [
{
"value": "15000",
"context": "Transaction.End",
"format": "Raw",
"measurand": "Energy.Active.Import.Register",
"location": "Outlet",
"unit": "Wh"
},
{
"value": "80",
"context": "Transaction.End",
"format": "Raw",
"measurand": "SoC",
"location": "EV",
"unit": "Percent"
}
]
}
]
}
]π₯ Response:
[
3,
"stop_001",
{
"idTagInfo": {
"status": "Accepted",
"expiryDate": "2024-12-31T23:59:59.000Z"
}
}
]π Key Fields:
transactionId(Integer): Transaction to stop (Required)idTag(String, Max 20): Authorization identifier (optional)meterStop(Integer): Energy meter value at stop (Wh) (Required)timestamp(DateTime): Transaction stop time (Required)reason(Enum): "EmergencyStop", "EVDisconnected", "HardReset", "Local", "Other", "PowerLoss", "Reboot", "Remote", "SoftReset", "UnlockCommand", "DeAuthorized" (optional)transactionData(Array): Historical meter values (optional)timestamp(DateTime): Sample timestamp (Required)sampledValue(Array): Sampled values (Required)value(String): Measured value (Required)context(Enum): "Interruption.Begin", "Interruption.End", "Sample.Clock", "Sample.Periodic", "Transaction.Begin", "Transaction.End", "Trigger", "Other" (optional)format(Enum): "Raw", "SignedData" (optional)measurand(Enum): "Energy.Active.Export.Register", "Energy.Active.Import.Register", "Energy.Reactive.Export.Register", "Energy.Reactive.Import.Register", "Energy.Active.Export.Interval", "Energy.Active.Import.Interval", "Energy.Reactive.Export.Interval", "Energy.Reactive.Import.Interval", "Power.Active.Export", "Power.Active.Import", "Power.Offered", "Power.Reactive.Export", "Power.Reactive.Import", "Power.Factor", "Current.Import", "Current.Export", "Current.Offered", "Voltage", "Frequency", "Temperature", "SoC", "RPM" (optional)phase(Enum): "L1", "L2", "L3", "N", "L1-N", "L2-N", "L3-N", "L1-L2", "L2-L3", "L3-L1" (optional)location(Enum): "Body", "Cable", "EV", "Inlet", "Outlet" (optional)unit(Enum): "Wh", "kWh", "varh", "kvarh", "W", "kW", "VA", "kVA", "var", "kvar", "A", "V", "Celsius", "Fahrenheit", "K", "Percent" (optional)
idTagInfo(Object): Authorization information (optional - Response)
Purpose: Send meter readings during charging
π€ Request:
[
2,
"meter_001",
"MeterValues",
{
"connectorId": 1,
"transactionId": 98765,
"meterValue": [
{
"timestamp": "2024-01-01T12:30:00.000Z",
"sampledValue": [
{
"value": "7500",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Energy.Active.Import.Register",
"location": "Outlet",
"unit": "Wh"
},
{
"value": "150",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Energy.Reactive.Import.Register",
"location": "Outlet",
"unit": "varh"
},
{
"value": "22.5",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Power.Active.Import",
"location": "Outlet",
"unit": "kW"
},
{
"value": "2.1",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Power.Reactive.Import",
"location": "Outlet",
"unit": "kvar"
},
{
"value": "0.95",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Power.Factor",
"location": "Outlet"
},
{
"value": "32.0",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Current.Import",
"phase": "L1",
"location": "Outlet",
"unit": "A"
},
{
"value": "31.8",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Current.Import",
"phase": "L2",
"location": "Outlet",
"unit": "A"
},
{
"value": "32.2",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Current.Import",
"phase": "L3",
"location": "Outlet",
"unit": "A"
},
{
"value": "230.5",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Voltage",
"phase": "L1-N",
"location": "Outlet",
"unit": "V"
},
{
"value": "229.8",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Voltage",
"phase": "L2-N",
"location": "Outlet",
"unit": "V"
},
{
"value": "231.2",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Voltage",
"phase": "L3-N",
"location": "Outlet",
"unit": "V"
},
{
"value": "50.0",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Frequency",
"location": "Outlet",
"unit": "Hz"
},
{
"value": "35.2",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Temperature",
"location": "Body",
"unit": "Celsius"
},
{
"value": "65",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "SoC",
"location": "EV",
"unit": "Percent"
},
{
"value": "1250",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Energy.Active.Import.Interval",
"location": "Outlet",
"unit": "Wh"
},
{
"value": "22000",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Power.Offered",
"location": "Outlet",
"unit": "W"
}
]
},
{
"timestamp": "2024-01-01T12:31:00.000Z",
"sampledValue": [
{
"value": "8750",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Energy.Active.Import.Register",
"location": "Outlet",
"unit": "Wh"
},
{
"value": "22.8",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Power.Active.Import",
"location": "Outlet",
"unit": "kW"
},
{
"value": "68",
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "SoC",
"location": "EV",
"unit": "Percent"
}
]
}
]
}
]π₯ Response:
[3, "meter_001", {}]π Key Fields:
connectorId(Integer): Connector identifier (Required)transactionId(Integer): Associated transaction (optional)meterValue(Array): Meter readings (Required)timestamp(DateTime): Reading timestamp (Required)sampledValue(Array): Sampled values (Required)value(String): Measured value (Required)context(Enum): "Interruption.Begin", "Interruption.End", "Sample.Clock", "Sample.Periodic", "Transaction.Begin", "Transaction.End", "Trigger", "Other" (optional)format(Enum): "Raw", "SignedData" (optional)measurand(Enum): "Energy.Active.Export.Register", "Energy.Active.Import.Register", "Energy.Reactive.Export.Register", "Energy.Reactive.Import.Register", "Energy.Active.Export.Interval", "Energy.Active.Import.Interval", "Energy.Reactive.Export.Interval", "Energy.Reactive.Import.Interval", "Power.Active.Export", "Power.Active.Import", "Power.Offered", "Power.Reactive.Export", "Power.Reactive.Import", "Power.Factor", "Current.Import", "Current.Export", "Current.Offered", "Voltage", "Frequency", "Temperature", "SoC", "RPM" (optional)phase(Enum): "L1", "L2", "L3", "N", "L1-N", "L2-N", "L3-N", "L1-L2", "L2-L3", "L3-L1" (optional)location(Enum): "Body", "Cable", "EV", "Inlet", "Outlet" (optional)unit(Enum): "Wh", "kWh", "varh", "kvarh", "W", "kW", "VA", "kVA", "var", "kvar", "A", "V", "Celsius", "Fahrenheit", "K", "Percent" (optional)
Purpose: Validate user authorization
π€ Request:
[
2,
"auth_001",
"Authorize",
{
"idTag": "RFID12345678"
}
]π₯ Response:
[
3,
"auth_001",
{
"idTagInfo": {
"status": "Accepted",
"expiryDate": "2024-12-31T23:59:59.000Z",
"parentIdTag": "MASTER001"
}
}
]π Key Fields:
idTag(String, Max 20): Authorization identifier (Required)idTagInfo(Object): Authorization information (Required - Response)status(Enum): "Accepted", "Blocked", "Expired", "Invalid", "ConcurrentTx" (Required)expiryDate(DateTime): Tag expiry date (optional)parentIdTag(String, Max 20): Parent tag identifier (optional)
Example 1: Blocked ID Tag:
[
3,
"auth_blocked_001",
{
"idTagInfo": {
"status": "Blocked",
"expiryDate": "2024-12-31T23:59:59.000Z",
"parentIdTag": "MASTER001"
}
}
]Example 2: Expired ID Tag:
[
3,
"auth_expired_001",
{
"idTagInfo": {
"status": "Expired",
"expiryDate": "2023-12-31T23:59:59.000Z"
}
}
]Example 3: Invalid ID Tag:
[
3,
"auth_invalid_001",
{
"idTagInfo": {
"status": "Invalid"
}
}
]Example 4: Concurrent Transaction:
[
3,
"auth_concurrent_001",
{
"idTagInfo": {
"status": "ConcurrentTx",
"expiryDate": "2024-12-31T23:59:59.000Z"
}
}
]Purpose: Exchange vendor-specific data
π€ Request:
[
2,
"data_001",
"DataTransfer",
{
"vendorId": "Tesla",
"messageId": "CustomMessage",
"data": "{\"temperature\": 25.5, \"humidity\": 60}"
}
]π₯ Response:
[
3,
"data_001",
{
"status": "Accepted",
"data": "{\"response\": \"OK\"}"
}
]π Key Fields:
vendorId(String, Max 255): Vendor identifier (Required)messageId(String, Max 50): Message identifier (optional)data(String): Custom data payload (optional)status(Enum): "Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId" (Required - Response)data(String): Response data payload (optional - Response)
Purpose: Report firmware update status
π€ Request:
[
2,
"firmware_status_001",
"FirmwareStatusNotification",
{
"status": "Downloading",
"requestId": 12345
}
]π₯ Response:
[3, "firmware_status_001", {}]π Key Fields:
status(Enum): "Downloaded", "DownloadFailed", "Downloading", "Idle", "InstallationFailed", "Installing", "Installed" (Required)requestId(Integer): Request identifier (optional)
Purpose: Report diagnostics collection status
π€ Request:
[
2,
"diagnostics_status_001",
"DiagnosticsStatusNotification",
{
"status": "Uploading"
}
]π₯ Response:
[3, "diagnostics_status_001", {}]π Key Fields:
status(Enum): "Idle", "Uploaded", "UploadFailed", "Uploading" (Required)
π Events sent from the Central System to the Charge Point
Purpose: Reset the charge point
π€ Request:
[
2,
"reset_001",
"Reset",
{
"type": "Hard"
}
]π₯ Response:
[
3,
"reset_001",
{
"status": "Accepted"
}
]π Key Fields:
type(Enum): "Hard", "Soft" (Required)status(Enum): "Accepted", "Rejected" (Required - Response)
Purpose: Unlock a connector
π€ Request:
[
2,
"unlock_001",
"UnlockConnector",
{
"connectorId": 1
}
]π₯ Response:
[
3,
"unlock_001",
{
"status": "Unlocked"
}
]π Key Fields:
connectorId(Integer): Connector to unlock (Required)status(Enum): "Unlocked", "UnlockFailed", "NotSupported" (Required - Response)
Purpose: Start charging remotely
π€ Request:
[
2,
"remote_start_001",
"RemoteStartTransaction",
{
"connectorId": 1,
"idTag": "RFID12345678",
"chargingProfile": {
"chargingProfileId": 1,
"transactionId": 98765,
"stackLevel": 0,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": "Daily",
"validFrom": "2024-01-01T00:00:00.000Z",
"validTo": "2024-01-02T00:00:00.000Z",
"chargingSchedule": {
"duration": 3600,
"startSchedule": "2024-01-01T12:00:00.000Z",
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"startPeriod": 0,
"limit": 32.0,
"numberPhases": 3
}
]
}
}
}
]π₯ Response:
[
3,
"remote_start_001",
{
"status": "Accepted"
}
]π Key Fields:
connectorId(Integer): Connector identifier (Required)idTag(String, Max 20): Authorization identifier (Required)chargingProfile(Object): Charging profile (optional)chargingProfileId(Integer): Profile identifier (Required)transactionId(Integer): Transaction identifier (optional)stackLevel(Integer): Profile stack level (Required)chargingProfilePurpose(Enum): "ChargePointMaxProfile", "TxDefaultProfile", "TxProfile" (Required)chargingProfileKind(Enum): "Absolute", "Recurring", "Relative" (Required)recurrencyKind(Enum): "Daily", "Weekly" (optional)validFrom(DateTime): Profile validity start (optional)validTo(DateTime): Profile validity end (optional)chargingSchedule(Object): Charging schedule (Required)duration(Integer): Schedule duration in seconds (optional)startSchedule(DateTime): Schedule start time (optional)chargingRateUnit(Enum): "A", "W" (Required)chargingSchedulePeriod(Array): Schedule periods (Required)startPeriod(Integer): Period start offset in seconds (Required)limit(Decimal): Charging rate limit (Required)numberPhases(Integer): Number of phases (optional)
minChargingRate(Decimal): Minimum charging rate (optional)
status(Enum): "Accepted", "Rejected" (Required - Response)
Example 1: Complex Multi-Period Charging Profile:
[
2,
"remote_start_advanced_001",
"RemoteStartTransaction",
{
"connectorId": 1,
"idTag": "RFID12345678",
"chargingProfile": {
"chargingProfileId": 2,
"stackLevel": 1,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"validFrom": "2024-01-01T00:00:00.000Z",
"validTo": "2024-01-02T00:00:00.000Z",
"chargingSchedule": {
"duration": 14400,
"startSchedule": "2024-01-01T18:00:00.000Z",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{
"startPeriod": 0,
"limit": 7400.0,
"numberPhases": 1
},
{
"startPeriod": 3600,
"limit": 11000.0,
"numberPhases": 1
},
{
"startPeriod": 7200,
"limit": 22000.0,
"numberPhases": 3
},
{
"startPeriod": 10800,
"limit": 7400.0,
"numberPhases": 1
}
],
"minChargingRate": 6000.0
}
}
}
]Example 2: Recurring Weekly Profile:
[
2,
"remote_start_recurring_001",
"RemoteStartTransaction",
{
"connectorId": 2,
"idTag": "FLEET-CARD-001",
"chargingProfile": {
"chargingProfileId": 3,
"stackLevel": 0,
"chargingProfilePurpose": "TxDefaultProfile",
"chargingProfileKind": "Recurring",
"recurrencyKind": "Weekly",
"validFrom": "2024-01-01T00:00:00.000Z",
"validTo": "2024-12-31T23:59:59.000Z",
"chargingSchedule": {
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"startPeriod": 0,
"limit": 6.0,
"numberPhases": 1
},
{
"startPeriod": 21600,
"limit": 16.0,
"numberPhases": 1
},
{
"startPeriod": 64800,
"limit": 32.0,
"numberPhases": 3
}
],
"minChargingRate": 6.0
}
}
}
]Example 3: Simple Start without Charging Profile:
[
2,
"remote_start_simple_001",
"RemoteStartTransaction",
{
"connectorId": 1,
"idTag": "GUEST-USER-001"
}
]Purpose: Stop charging remotely
π€ Request:
[
2,
"remote_stop_001",
"RemoteStopTransaction",
{
"transactionId": 98765
}
]π₯ Response:
[
3,
"remote_stop_001",
{
"status": "Accepted"
}
]π Key Fields:
transactionId(Integer): Transaction to stopstatus(Enum): "Accepted", "Rejected"
Purpose: Change charge point configuration
π€ Request:
[
2,
"config_001",
"ChangeConfiguration",
{
"key": "HeartbeatInterval",
"value": "60"
}
]π₯ Response:
[
3,
"config_001",
{
"status": "Accepted"
}
]π Key Fields:
key(String, Max 50): Configuration key (Required)value(String, Max 500): Configuration value (Required)status(Enum): "Accepted", "Rejected", "RebootRequired", "NotSupported" (Required - Response)
Purpose: Get charge point configuration
π€ Request:
[
2,
"get_config_001",
"GetConfiguration",
{
"key": [
"HeartbeatInterval",
"MeterValueSampleInterval",
"InvalidKey",
"UnknownSetting"
]
}
]π₯ Response:
[
3,
"get_config_001",
{
"configurationKey": [
{
"key": "HeartbeatInterval",
"readonly": false,
"value": "300"
},
{
"key": "MeterValueSampleInterval",
"readonly": false,
"value": "60"
},
{
"key": "AuthorizeRemoteTxRequests",
"readonly": true,
"value": "true"
},
{
"key": "ClockAlignedDataInterval",
"readonly": false,
"value": "900"
},
{
"key": "ConnectionTimeOut",
"readonly": false,
"value": "60"
},
{
"key": "LocalAuthorizeOffline",
"readonly": false,
"value": "true"
},
{
"key": "LocalPreAuthorize",
"readonly": false,
"value": "false"
},
{
"key": "MeterValuesAlignedData",
"readonly": false,
"value": "Energy.Active.Import.Register,Power.Active.Import"
},
{
"key": "MeterValuesSampledData",
"readonly": false,
"value": "Energy.Active.Import.Register,Power.Active.Import,Current.Import,Voltage,SoC"
},
{
"key": "NumberOfConnectors",
"readonly": true,
"value": "2"
},
{
"key": "ResetRetries",
"readonly": false,
"value": "3"
},
{
"key": "StopTransactionOnEVSideDisconnect",
"readonly": false,
"value": "true"
},
{
"key": "StopTransactionOnInvalidId",
"readonly": false,
"value": "true"
},
{
"key": "StopTxnAlignedData",
"readonly": false,
"value": "Energy.Active.Import.Register"
},
{
"key": "StopTxnSampledData",
"readonly": false,
"value": "Energy.Active.Import.Register,Power.Active.Import"
},
{
"key": "TransactionMessageAttempts",
"readonly": false,
"value": "3"
},
{
"key": "TransactionMessageRetryInterval",
"readonly": false,
"value": "60"
},
{
"key": "UnlockConnectorOnEVSideDisconnect",
"readonly": false,
"value": "true"
}
],
"unknownKey": ["InvalidKey", "UnknownSetting"]
}
]π Key Fields:
key(Array): Configuration keys to retrieve (optional)configurationKey(Array): Configuration items (Required - Response)key(String, Max 50): Configuration key (Required)readonly(Boolean): Is key read-only (Required)value(String, Max 500): Configuration value (optional)
unknownKey(Array): Unknown keys (Required - Response)
Purpose: Request diagnostic data from charge point
π€ Request:
[
2,
"diagnostics_001",
"GetDiagnostics",
{
"location": "ftp://server.com/diagnostics/",
"retries": 3,
"retryInterval": 60,
"startTime": "2024-01-01T00:00:00.000Z",
"stopTime": "2024-01-01T23:59:59.000Z"
}
]π₯ Response:
[
3,
"diagnostics_001",
{
"fileName": "diagnostics_CP001_20240101.log"
}
]π Key Fields:
location(String): Upload location URIretries(Integer): Number of retries (optional)retryInterval(Integer): Retry interval in seconds (optional)startTime(DateTime): Start time for diagnostic data (optional)stopTime(DateTime): Stop time for diagnostic data (optional)fileName(String): Generated diagnostic file name (optional)
Purpose: Update charge point firmware
π€ Request:
[
2,
"firmware_001",
"UpdateFirmware",
{
"location": "ftp://server.com/firmware/charger_v2.0.bin",
"retries": 3,
"retrieveDate": "2024-01-01T02:00:00.000Z",
"retryInterval": 300
}
]π₯ Response:
[3, "firmware_001", {}]π Key Fields:
location(String): Firmware download URIretries(Integer): Number of retries (optional)retrieveDate(DateTime): When to retrieve firmwareretryInterval(Integer): Retry interval in seconds (optional)
Purpose: Reserve a connector for a specific user
π€ Request:
[
2,
"reserve_001",
"ReserveNow",
{
"connectorId": 1,
"expiryDate": "2024-01-01T15:00:00.000Z",
"idTag": "RFID12345678",
"reservationId": 12345,
"parentIdTag": "MASTER001"
}
]π₯ Response:
[
3,
"reserve_001",
{
"status": "Accepted"
}
]π Key Fields:
connectorId(Integer): Connector to reserveexpiryDate(DateTime): Reservation expiry timeidTag(String, Max 20): Authorized user identifierreservationId(Integer): Unique reservation identifierparentIdTag(String, Max 20): Parent identifier (optional)status(Enum): "Accepted", "Faulted", "Occupied", "Rejected", "Unavailable"
Purpose: Cancel an existing reservation
π€ Request:
[
2,
"cancel_res_001",
"CancelReservation",
{
"reservationId": 12345
}
]π₯ Response:
[
3,
"cancel_res_001",
{
"status": "Accepted"
}
]π Key Fields:
reservationId(Integer): Reservation to cancelstatus(Enum): "Accepted", "Rejected"
Purpose: Clear authorization cache
π€ Request:
[2, "clear_cache_001", "ClearCache", {}]π₯ Response:
[
3,
"clear_cache_001",
{
"status": "Accepted"
}
]π Key Fields:
status(Enum): "Accepted", "Rejected"
Purpose: Get local authorization list version
π€ Request:
[2, "get_list_001", "GetLocalListVersion", {}]π₯ Response:
[
3,
"get_list_001",
{
"listVersion": 42
}
]π Key Fields:
listVersion(Integer): Current list version (-1 if no list)
Purpose: Update local authorization list
π€ Request:
[
2,
"send_list_001",
"SendLocalList",
{
"listVersion": 43,
"updateType": "Differential",
"localAuthorizationList": [
{
"idTag": "RFID12345678",
"idTagInfo": {
"status": "Accepted",
"expiryDate": "2024-12-31T23:59:59.000Z",
"parentIdTag": "MASTER001"
}
},
{
"idTag": "RFID87654321",
"idTagInfo": {
"status": "Blocked"
}
}
]
}
]π₯ Response:
[
3,
"send_list_001",
{
"status": "Accepted"
}
]π Key Fields:
listVersion(Integer): New list versionupdateType(Enum): "Differential", "Full"localAuthorizationList(Array): Authorization entriesidTag(String, Max 20): Tag identifieridTagInfo(Object): Authorization info (same as StartTransaction)
status(Enum): "Accepted", "Failed", "NotSupported", "VersionMismatch"
Purpose: Set charging profile for power management
π€ Request:
[
2,
"set_profile_001",
"SetChargingProfile",
{
"connectorId": 1,
"csChargingProfiles": {
"chargingProfileId": 1,
"transactionId": 98765,
"stackLevel": 0,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": "Daily",
"validFrom": "2024-01-01T00:00:00.000Z",
"validTo": "2024-01-02T00:00:00.000Z",
"chargingSchedule": {
"duration": 7200,
"startSchedule": "2024-01-01T12:00:00.000Z",
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"startPeriod": 0,
"limit": 32.0,
"numberPhases": 3
},
{
"startPeriod": 3600,
"limit": 16.0,
"numberPhases": 1
}
],
"minChargingRate": 6.0
}
}
}
]π₯ Response:
[
3,
"set_profile_001",
{
"status": "Accepted"
}
]π Key Fields:
connectorId(Integer): Connector ID (0 = entire charge point)csChargingProfiles(Object): Charging profile (same structure as RemoteStartTransaction)status(Enum): "Accepted", "Rejected", "NotSupported"
Purpose: Remove charging profiles
π€ Request:
[
2,
"clear_profile_001",
"ClearChargingProfile",
{
"id": 1,
"connectorId": 1,
"chargingProfilePurpose": "TxProfile",
"stackLevel": 0
}
]π₯ Response:
[
3,
"clear_profile_001",
{
"status": "Accepted"
}
]π Key Fields:
id(Integer): Charging profile ID (optional)connectorId(Integer): Connector ID (optional)chargingProfilePurpose(Enum): Profile purpose (optional)stackLevel(Integer): Stack level (optional)status(Enum): "Accepted", "Unknown"
Purpose: Get composite charging schedule
π€ Request:
[
2,
"get_schedule_001",
"GetCompositeSchedule",
{
"connectorId": 1,
"duration": 3600,
"chargingRateUnit": "A"
}
]π₯ Response:
[
3,
"get_schedule_001",
{
"status": "Accepted",
"connectorId": 1,
"scheduleStart": "2024-01-01T12:00:00.000Z",
"chargingSchedule": {
"duration": 3600,
"startSchedule": "2024-01-01T12:00:00.000Z",
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"startPeriod": 0,
"limit": 32.0,
"numberPhases": 3
},
{
"startPeriod": 1800,
"limit": 16.0,
"numberPhases": 1
}
]
}
}
]π Key Fields:
connectorId(Integer): Connector identifierduration(Integer): Schedule duration in secondschargingRateUnit(Enum): "A", "W"status(Enum): "Accepted", "Rejected"scheduleStart(DateTime): Schedule start timechargingSchedule(Object): Composite schedule
Purpose: Trigger specific message from charge point
π€ Request:
[
2,
"trigger_001",
"TriggerMessage",
{
"requestedMessage": "StatusNotification",
"connectorId": 1
}
]π₯ Response:
[
3,
"trigger_001",
{
"status": "Accepted"
}
]π Key Fields:
requestedMessage(Enum): "BootNotification", "DiagnosticsStatusNotification", "FirmwareStatusNotification", "Heartbeat", "MeterValues", "StatusNotification" (Required)connectorId(Integer): Connector ID (optional, required for specific messages like StatusNotification, MeterValues)status(Enum): "Accepted", "Rejected", "NotImplemented" (Required - Response)
Purpose: Change connector availability (operative/inoperative)
π€ Request:
[
2,
"change_avail_001",
"ChangeAvailability",
{
"connectorId": 1,
"type": "Inoperative"
}
]π₯ Response:
[
3,
"change_avail_001",
{
"status": "Accepted"
}
]π Key Fields:
connectorId(Integer): Connector to change (0 = entire charge point)type(Enum): "Inoperative", "Operative"status(Enum): "Accepted", "Rejected", "Scheduled"
π οΈ Ready-to-use code examples for Node.js, Python, and Java
π Required Dependencies:
npm install ws uuidπ Complete OCPP 1.6 Client:
const WebSocket = require("ws");
const { v4: uuidv4 } = require("uuid");
class OCPPClient {
constructor(url, chargePointId) {
this.url = url;
this.chargePointId = chargePointId;
this.ws = null;
this.messageQueue = new Map();
this.heartbeatInterval = null;
}
connect() {
return new Promise((resolve, reject) => {
const wsUrl = `${this.url}/${this.chargePointId}`;
this.ws = new WebSocket(wsUrl, ["ocpp1.6"]);
this.ws.on("open", () => {
console.log("π Connected to OCPP Server");
this.sendBootNotification()
.then(() => {
this.startHeartbeat();
resolve();
})
.catch(reject);
});
this.ws.on("message", (data) => {
this.handleMessage(JSON.parse(data.toString()));
});
this.ws.on("error", (error) => {
console.error("β WebSocket Error:", error);
reject(error);
});
this.ws.on("close", () => {
console.log("π Connection Closed");
this.stopHeartbeat();
});
});
}
// π Send Heartbeat
sendHeartbeat() {
return this.sendCall("Heartbeat", {});
}
// π Send BootNotification
sendBootNotification() {
const payload = {
chargePointVendor: "YourCompany",
chargePointModel: "FastCharger-Pro",
chargePointSerialNumber: "FC-001234",
chargeBoxSerialNumber: "CB-001234",
firmwareVersion: "1.0.0",
iccid: "8944501234567890123",
imsi: "234567890123456",
meterType: "EnergyMeter-X1",
meterSerialNumber: "EM-001234",
};
return this.sendCall("BootNotification", payload);
}
// π Send BootNotification - Response Scenarios
sendBootNotificationPending() {
const payload = {
currentTime: "2024-01-01T12:00:00.000Z",
interval: 300,
status: "Pending",
};
return this.sendCall("BootNotification", payload);
}
sendBootNotificationRejected() {
const payload = {
currentTime: "2024-01-01T12:00:00.000Z",
interval: 86400,
status: "Rejected",
};
return this.sendCall("BootNotification", payload);
}
// π Send StatusNotification
sendStatusNotification(connectorId, status, errorCode = "NoError") {
const payload = {
connectorId: connectorId,
status: status,
errorCode: errorCode,
timestamp: new Date().toISOString(),
info: `Connector ${connectorId} is ${status}`,
};
return this.sendCall("StatusNotification", payload);
}
// π Send StartTransaction
sendStartTransaction(connectorId, idTag, meterStart) {
const payload = {
connectorId: connectorId,
idTag: idTag,
meterStart: meterStart,
timestamp: new Date().toISOString(),
};
return this.sendCall("StartTransaction", payload);
}
// π Send StopTransaction
sendStopTransaction(transactionId, idTag, meterStop, reason = "Local") {
const payload = {
transactionId: transactionId,
idTag: idTag,
meterStop: meterStop,
timestamp: new Date().toISOString(),
reason: reason,
};
return this.sendCall("StopTransaction", payload);
}
// π Send MeterValues
sendMeterValues(connectorId, transactionId, meterValues) {
const payload = {
connectorId: connectorId,
transactionId: transactionId,
meterValue: [
{
timestamp: new Date().toISOString(),
sampledValue: meterValues,
},
],
};
return this.sendCall("MeterValues", payload);
}
// π§ Send Authorize
sendAuthorize(idTag) {
const payload = { idTag: idTag };
return this.sendCall("Authorize", payload);
}
// Generic method to send CALL messages
sendCall(action, payload) {
return new Promise((resolve, reject) => {
const messageId = uuidv4();
const message = [2, messageId, action, payload];
this.messageQueue.set(messageId, { resolve, reject });
this.ws.send(JSON.stringify(message));
console.log(`π€ Sent ${action}:`, payload);
// Timeout after 30 seconds
setTimeout(() => {
if (this.messageQueue.has(messageId)) {
this.messageQueue.delete(messageId);
reject(new Error(`Timeout waiting for ${action} response`));
}
}, 30000);
});
}
// Handle incoming messages
handleMessage(message) {
const [messageType, messageId, actionOrPayload, payload] = message;
if (messageType === 3) {
// CALLRESULT
console.log(`π₯ Received Response:`, actionOrPayload);
if (this.messageQueue.has(messageId)) {
const { resolve } = this.messageQueue.get(messageId);
this.messageQueue.delete(messageId);
resolve(actionOrPayload);
}
} else if (messageType === 4) {
// CALLERROR
console.error(`β Received Error:`, actionOrPayload, payload);
if (this.messageQueue.has(messageId)) {
const { reject } = this.messageQueue.get(messageId);
this.messageQueue.delete(messageId);
reject(new Error(`${actionOrPayload}: ${payload}`));
}
} else if (messageType === 2) {
// CALL (from server)
this.handleServerCall(messageId, actionOrPayload, payload);
}
}
// Handle server-initiated calls
handleServerCall(messageId, action, payload) {
console.log(`π¨ Received ${action}:`, payload);
let response = {};
switch (action) {
case "Reset":
response = { status: "Accepted" };
console.log("π Resetting charge point...");
break;
case "UnlockConnector":
response = { status: "Unlocked" };
console.log(`π Unlocking connector ${payload.connectorId}`);
break;
case "RemoteStartTransaction":
response = { status: "Accepted" };
console.log("π Starting remote transaction...");
break;
case "RemoteStopTransaction":
response = { status: "Accepted" };
console.log("π΄ Stopping remote transaction...");
break;
case "ChangeConfiguration":
response = { status: "Accepted" };
console.log(
`βοΈ Configuration changed: ${payload.key} = ${payload.value}`,
);
break;
case "GetConfiguration":
response = {
configurationKey: [
{
key: "HeartbeatInterval",
readonly: false,
value: "300",
},
],
unknownKey: [],
};
break;
case "ChangeAvailability":
response = { status: "Accepted" };
console.log(
`π§ Connector ${payload.connectorId} availability set to ${payload.type}`,
);
break;
case "GetDiagnostics":
response = { fileName: "diagnostics_CP001.log" };
console.log(`π Diagnostics requested to ${payload.location}`);
break;
case "UpdateFirmware":
response = {};
console.log(
`π Firmware update requested from ${payload.location}`,
);
break;
case "ReserveNow":
response = { status: "Accepted" };
console.log(
`π
Connector ${payload.connectorId} reserved for ${payload.idTag}`,
);
break;
case "CancelReservation":
response = { status: "Accepted" };
console.log(
`β Reservation ${payload.reservationId} cancelled`,
);
break;
case "ClearCache":
response = { status: "Accepted" };
console.log("ποΈ Authorization cache cleared");
break;
case "GetLocalListVersion":
response = { listVersion: 1 };
console.log("π Local list version requested");
break;
case "SendLocalList":
response = { status: "Accepted" };
console.log(
`π€ Local list updated to version ${payload.listVersion}`,
);
break;
case "SetChargingProfile":
response = { status: "Accepted" };
console.log(
`β‘ Charging profile set for connector ${payload.connectorId}`,
);
break;
case "ClearChargingProfile":
response = { status: "Accepted" };
console.log("ποΈ Charging profiles cleared");
break;
case "GetCompositeSchedule":
response = {
status: "Accepted",
connectorId: payload.connectorId,
scheduleStart: new Date().toISOString(),
chargingSchedule: {
duration: payload.duration,
chargingRateUnit: payload.chargingRateUnit || "A",
chargingSchedulePeriod: [
{
startPeriod: 0,
limit: 32.0,
numberPhases: 3,
},
],
},
};
console.log(
`π Composite schedule requested for connector ${payload.connectorId}`,
);
break;
case "TriggerMessage":
response = { status: "Accepted" };
console.log(`π― Trigger message: ${payload.requestedMessage}`);
break;
default:
response = { status: "NotSupported" };
}
// Send CALLRESULT
const result = [3, messageId, response];
this.ws.send(JSON.stringify(result));
console.log(`π€ Sent Response:`, response);
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat().catch(console.error);
}, 300000); // 5 minutes
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
disconnect() {
this.stopHeartbeat();
if (this.ws) {
this.ws.close();
}
}
}
// π Usage Example
async function main() {
const client = new OCPPClient("ws://localhost:8080/ocpp", "CHARGER_001");
try {
await client.connect();
// Send status notification
await client.sendStatusNotification(1, "Available");
// Simulate charging session
const authResult = await client.sendAuthorize("RFID12345678");
console.log("π Authorization:", authResult);
if (authResult.idTagInfo.status === "Accepted") {
const startResult = await client.sendStartTransaction(
1,
"RFID12345678",
1000,
);
console.log("π Transaction Started:", startResult);
// Send meter values every 60 seconds
const meterInterval = setInterval(async () => {
const meterValues = [
{
value: (Math.random() * 50 + 10).toFixed(2),
measurand: "Power.Active.Import",
unit: "kW",
},
{
value: (Math.random() * 100 + 200).toFixed(1),
measurand: "Voltage",
unit: "V",
},
];
await client.sendMeterValues(
1,
startResult.transactionId,
meterValues,
);
}, 60000);
// Stop transaction after 10 minutes
setTimeout(async () => {
clearInterval(meterInterval);
await client.sendStopTransaction(
startResult.transactionId,
"RFID12345678",
15000,
"Local",
);
await client.sendStatusNotification(1, "Available");
}, 600000);
}
} catch (error) {
console.error("π₯ Error:", error);
}
}
// Uncomment to run
// main();pip install websockets asyncio uuidπ Complete OCPP 1.6 Client:
import asyncio
import websockets
import json
import uuid
from datetime import datetime
from typing import Dict, Any, Optional
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class OCPPClient:
def __init__(self, url: str, charge_point_id: str):
self.url = url
self.charge_point_id = charge_point_id
self.websocket = None
self.message_queue: Dict[str, asyncio.Future] = {}
self.heartbeat_task = None
async def connect(self):
"""π Connect to OCPP Server"""
ws_url = f"{self.url}/{self.charge_point_id}"
self.websocket = await websockets.connect(
ws_url,
subprotocols=["ocpp1.6"]
)
logger.info("π Connected to OCPP Server")
# Start message handler
asyncio.create_task(self._message_handler())
# Send boot notification
boot_response = await self.send_boot_notification()
logger.info(f"π Boot Response: {boot_response}")
# Start heartbeat
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
async def _message_handler(self):
"""π¨ Handle incoming messages"""
try:
async for message in self.websocket:
data = json.loads(message)
await self._handle_message(data)
except websockets.exceptions.ConnectionClosed:
logger.info("π Connection closed")
except Exception as e:
logger.error(f"β Message handler error: {e}")
async def _handle_message(self, message: list):
"""Process incoming OCPP messages"""
message_type, message_id = message[0], message[1]
if message_type == 3: # CALLRESULT
payload = message[2]
logger.info(f"π₯ Received Response: {payload}")
if message_id in self.message_queue:
future = self.message_queue.pop(message_id)
future.set_result(payload)
elif message_type == 4: # CALLERROR
error_code, error_description = message[2], message[3]
logger.error(f"β Received Error: {error_code} - {error_description}")
if message_id in self.message_queue:
future = self.message_queue.pop(message_id)
future.set_exception(Exception(f"{error_code}: {error_description}"))
elif message_type == 2: # CALL (from server)
action, payload = message[2], message[3]
await self._handle_server_call(message_id, action, payload)
async def _handle_server_call(self, message_id: str, action: str, payload: dict):
"""π― Handle server-initiated calls"""
logger.info(f"π¨ Received {action}: {payload}")
response = {}
if action == "Reset":
response = {"status": "Accepted"}
logger.info("π Resetting charge point...")
elif action == "UnlockConnector":
response = {"status": "Unlocked"}
logger.info(f"π Unlocking connector {payload['connectorId']}")
elif action == "RemoteStartTransaction":
response = {"status": "Accepted"}
logger.info("π Starting remote transaction...")
elif action == "RemoteStopTransaction":
response = {"status": "Accepted"}
logger.info("π΄ Stopping remote transaction...")
elif action == "ChangeConfiguration":
response = {"status": "Accepted"}
logger.info(f"βοΈ Configuration changed: {payload['key']} = {payload['value']}")
elif action == "GetConfiguration":
response = {
"configurationKey": [
{
"key": "HeartbeatInterval",
"readonly": False,
"value": "300"
},
{
"key": "MeterValueSampleInterval",
"readonly": False,
"value": "60"
}
],
"unknownKey": []
}
elif action == "ChangeAvailability":
response = {"status": "Accepted"}
logger.info(f"π§ Connector {payload['connectorId']} availability set to {payload['type']}")
elif action == "GetDiagnostics":
response = {"fileName": "diagnostics_CP001.log"}
logger.info(f"π Diagnostics requested to {payload['location']}")
elif action == "UpdateFirmware":
response = {}
logger.info(f"π Firmware update requested from {payload['location']}")
elif action == "ReserveNow":
response = {"status": "Accepted"}
logger.info(f"π
Connector {payload['connectorId']} reserved for {payload['idTag']}")
elif action == "CancelReservation":
response = {"status": "Accepted"}
logger.info(f"β Reservation {payload['reservationId']} cancelled")
elif action == "ClearCache":
response = {"status": "Accepted"}
logger.info("ποΈ Authorization cache cleared")
elif action == "GetLocalListVersion":
response = {"listVersion": 1}
logger.info("π Local list version requested")
elif action == "SendLocalList":
response = {"status": "Accepted"}
logger.info(f"π€ Local list updated to version {payload['listVersion']}")
elif action == "SetChargingProfile":
response = {"status": "Accepted"}
logger.info(f"β‘ Charging profile set for connector {payload['connectorId']}")
elif action == "ClearChargingProfile":
response = {"status": "Accepted"}
logger.info("ποΈ Charging profiles cleared")
elif action == "GetCompositeSchedule":
response = {
"status": "Accepted",
"connectorId": payload["connectorId"],
"scheduleStart": datetime.utcnow().isoformat() + "Z",
"chargingSchedule": {
"duration": payload["duration"],
"chargingRateUnit": payload.get("chargingRateUnit", "A"),
"chargingSchedulePeriod": [
{
"startPeriod": 0,
"limit": 32.0,
"numberPhases": 3
},
],
},
};
logger.info(f"π Composite schedule requested for connector {payload['connectorId']}")
elif action == "TriggerMessage":
response = {"status": "Accepted"}
logger.info(f"π― Trigger message: {payload['requestedMessage']}")
else:
response = {"status": "NotSupported"}
# Send CALLRESULT
result = [3, message_id, response]
await self.websocket.send(json.dumps(result))
logger.info(f"π€ Sent Response: {response}")
async def _send_call(self, action: str, payload: dict) -> dict:
"""π€ Send CALL message and wait for response"""
message_id = str(uuid.uuid4())
message = [2, message_id, action, payload]
# Create future for response
future = asyncio.Future()
self.message_queue[message_id] = future
# Send message
await self.websocket.send(json.dumps(message))
logger.info(f"π€ Sent {action}: {payload}")
# Wait for response with timeout
try:
response = await asyncio.wait_for(future, timeout=30.0)
return response
} except asyncio.TimeoutError {
self.message_queue.pop(message_id, None)
raise Exception(f"Timeout waiting for {action} response")
}
}
async def send_heartbeat(self) -> dict:
"""π Send Heartbeat"""
return await self._send_call("Heartbeat", {})
async def send_boot_notification(self) -> dict:
"""π Send BootNotification"""
payload = {
"chargePointVendor": "YourCompany",
"chargePointModel": "FastCharger-Pro",
"chargePointSerialNumber": "FC-001234",
"chargeBoxSerialNumber": "CB-001234",
"firmwareVersion": "1.0.0",
"iccid": "8944501234567890123",
"imsi": "234567890123456",
"meterType": "EnergyMeter-X1",
"meterSerialNumber": "EM-001234"
}
return await this._send_call("BootNotification", payload)
async def send_boot_notification_pending(self) -> dict:
"""π Send BootNotification - Pending Scenario"""
payload = {
"currentTime": "2024-01-01T12:00:00.000Z",
"interval": 300,
"status": "Pending"
}
return await this._send_call("BootNotification", payload)
async def send_boot_notification_rejected(self) -> dict:
"""π Send BootNotification - Rejected Scenario"""
payload = {
"currentTime": "2024-01-01T12:00:00.000Z",
"interval": 86400,
"status": "Rejected"
}
return await this._send_call("BootNotification", payload)
async def send_status_notification(self, connector_id: int, status: str,
error_code: str = "NoError") -> dict:
"""π Send StatusNotification"""
payload = {
"connectorId": connector_id,
"status": status,
"errorCode": error_code,
"timestamp": datetime.utcnow().isoformat() + "Z",
"info": f"Connector {connector_id} is {status}"
}
return await this._send_call("StatusNotification", payload)
async def send_start_transaction(self, connector_id: int, id_tag: str,
meter_start: int) -> dict:
"""π Send StartTransaction"""
payload = {
"connectorId": connector_id,
"idTag": id_tag,
"meterStart": meter_start,
"timestamp": datetime.utcnow().isoformat() + "Z"
}
return await this._send_call("StartTransaction", payload)
async def send_stop_transaction(self, transaction_id: int, id_tag: str,
meter_stop: int, reason: str = "Local") -> dict:
"""π Send StopTransaction"""
payload = {
"transactionId": transaction_id,
"idTag": id_tag,
"meterStop": meter_stop,
"timestamp": datetime.utcnow().isoformat() + "Z",
"reason": reason
}
return await this._send_call("StopTransaction", payload)
async def send_meter_values(self, connector_id: int, transaction_id: int,
meter_values: list) -> dict:
"""π Send MeterValues"""
payload = {
"connectorId": connector_id,
"transactionId": transaction_id,
"meterValue": [{
"timestamp": datetime.utcnow().isoformat() + "Z",
"sampledValue": meter_values
}]
}
return await this._send_call("MeterValues", payload)
async def send_authorize(self, id_tag: str) -> dict:
"""π§ Send Authorize"""
payload = {"idTag": id_tag}
return await this._send_call("Authorize", payload)
async def _heartbeat_loop(self):
"""π Periodic heartbeat loop"""
while True:
try:
await asyncio.sleep(300) # 5 minutes
await self.send_heartbeat()
except Exception as e:
logger.error(f"π Heartbeat error: {e}")
break
async def disconnect(self):
"""π Disconnect from server"""
if self.heartbeat_task:
self.heartbeat_task.cancel()
if self.websocket:
await self.websocket.close()
logger.info("π Disconnected from OCPP Server")
# π Usage Example
async def main():
client = OCPPClient("ws://localhost:8080/ocpp", "CHARGER_001")
try:
await client.connect()
# Send status notification
await client.send_status_notification(1, "Available")
# Simulate charging session
auth_result = await client.send_authorize("RFID12345678")
logger.info(f"π Authorization: {auth_result}")
if auth_result["idTagInfo"]["status"] == "Accepted":
start_result = await client.send_start_transaction(1, "RFID12345678", 1000)
logger.info(f"π Transaction Started: {start_result}")
# Send meter values periodically
for i in range(5):
meter_values = [
{
"value": f"{(i + 1) * 10.5:.2f}",
"measurand": "Power.Active.Import",
"unit": "kW"
},
{
"value": f"{230 + i * 2:.1f}",
"measurand": "Voltage",
"unit": "V"
}
]
await client.send_meter_values(1, start_result["transactionId"], meter_values)
await asyncio.sleep(60) # Wait 1 minute
# Stop transaction
await client.send_stop_transaction(
start_result["transactionId"],
"RFID12345678",
15000,
"Local"
)
await client.send_status_notification(1, "Available")
# Keep connection alive for demonstration
await asyncio.sleep(10)
except Exception as e:
logger.error(f"π₯ Error: {e}")
finally:
await client.disconnect()
# Uncomment to run
# if __name__ == "__main__":
# asyncio.run(main()){
"errorCode": "GenericError",
"errorDescription": "Generic error message",
"errorDetails": {}
}Standard Error Codes:
NotImplemented- Feature not supportedNotSupported- Operation not supportedInternalError- Server internal errorProtocolError- Protocol violationSecurityError- Security validation failedFormationViolation- Message format invalidPropertyConstraintViolation- Property validation failedOccurenceConstraintViolation- Required field missingTypeConstraintViolation- Data type mismatchGenericError- Catch-all error
// Node.js Error Handling
websocket.on("error", (error) => {
console.error("π¨ WebSocket Error:", error.message);
switch (error.code) {
case "ECONNREFUSED":
console.log("π Attempting reconnection in 30 seconds...");
setTimeout(() => reconnect(), 30000);
break;
case "ENOTFOUND":
console.error("β Server not found. Check URL and DNS.");
break;
default:
console.error("β Unknown connection error:", error);
}
});// CALLERROR Response
[4, "12345", "FormationViolation", "Invalid timestamp format", {}]// Handle transaction state conflicts
if (request.action === "StartTransaction") {
if (connector.status !== "Available") {
return [
4,
messageId,
"GenericError",
`Connector ${connectorId} not available. Current status: ${connector.status}`,
{},
];
}
}Problem: WebSocket connection fails
# Check connectivity
ping your-ocpp-server.com
telnet your-ocpp-server.com 8080Solutions:
- Verify server URL and port
- Check firewall settings
- Validate network connectivity
- Test with different security profile
Problem: BootNotification rejected
{
"status": "Rejected",
"currentTime": "2024-01-15T10:30:00.000Z",
"interval": 86400
}Solutions:
- Check charge point model/vendor in whitelist
- Validate serial number format
- Verify security credentials
- Check server configuration
Problem: StartTransaction fails
// Common validation checks
function validateStartTransaction(request) {
const errors = [];
if (!request.idTag || request.idTag.length > 20) {
errors.push("Invalid idTag length");
}
if (!request.meterStart || request.meterStart < 0) {
errors.push("Invalid meterStart value");
}
if (!request.timestamp || !Date.parse(request.timestamp)) {
errors.push("Invalid timestamp format");
}
return errors;
}Problem: MeterValues rejected or ignored
// Ensure proper sampledValue structure
{
"meterValue": [
{
"timestamp": "2024-01-15T10:30:00.000Z",
"sampledValue": [
{
"value": "15.6",
"measurand": "Power.Active.Import",
"unit": "kW",
"context": "Sample.Periodic"
}
]
}
]
}Common Issues:
- Missing required fields
- Invalid measurand values
- Incorrect timestamp format
- Wrong unit for measurand
- Invalid numeric values
// Use message queuing for high-frequency data
class MessageQueue {
constructor(maxSize = 1000) {
this.queue = [];
this.maxSize = maxSize;
this.processing = false;
}
async enqueue(message) {
if (this.queue.length >= this.maxSize) {
console.warn("β οΈ Message queue full, dropping oldest message");
this.queue.shift();
}
this.queue.push(message);
if (!this.processing) {
await this.processQueue();
}
}
async processQueue() {
this.processing = true;
while (this.queue.length > 0) {
const message = this.queue.shift();
try {
await this.sendMessage(message);
await new Promise((resolve) => setTimeout(resolve, 100)); // Rate limiting
} catch (error) {
console.error("Failed to send message:", error);
// Re-queue on failure
this.queue.unshift(message);
break;
}
}
this.processing = false;
}
}// Smart meter value reporting
class SmartMeterReporting {
constructor() {
this.lastValues = {};
this.thresholds = {
"Power.Active.Import": 1.0, // 1kW threshold
"Current.Import": 5.0, // 5A threshold
Voltage: 10.0, // 10V threshold
};
}
shouldSendMeterValue(measurand, newValue) {
const lastValue = this.lastValues[measurand] || 0;
const threshold = this.thresholds[measurand] || 1.0;
// Send if change exceeds threshold or 5 minutes passed
const timeSinceLastSend = Date.now() - (this.lastSent || 0);
const significantChange = Math.abs(newValue - lastValue) > threshold;
const timeExpired = timeSinceLastSend > 300000; // 5 minutes
return significantChange || timeExpired;
}
}// Comprehensive input validation
function validateOCPPMessage(message) {
const [messageType, messageId, action, payload] = message;
// Basic structure validation
if (!Array.isArray(message) || message.length < 3) {
throw new Error("Invalid message structure");
}
// Message type validation
if (![2, 3, 4].includes(messageType)) {
throw new Error("Invalid message type");
}
// Message ID validation
if (typeof messageId !== "string" || messageId.length === 0) {
throw new Error("Invalid message ID");
}
// Action validation for CALL messages
if (messageType === 2) {
if (typeof action !== "string" || !VALID_ACTIONS.includes(action)) {
throw new Error(`Invalid action: ${action}`);
}
}
// Payload validation
if (typeof payload !== "object" || payload === null) {
throw new Error("Invalid payload");
}
return true;
}// Secure WebSocket configuration
const secureConfig = {
// Enable certificate validation
rejectUnauthorized: true,
// Set connection timeout
handshakeTimeout: 30000,
// Enable compression
perMessageDeflate: true,
// Set frame size limits
maxReceivedFrameSize: 131072,
maxReceivedMessageSize: 1048576,
// Rate limiting
maxConnections: 100,
// Authentication
verifyClient: (info) => {
// Implement custom authentication logic
return validateClientCertificate(info.req);
},
};// Health monitoring system
class HealthMonitor {
constructor() {
this.metrics = {
messagesSent: 0,
messagesReceived: 0,
errors: 0,
lastHeartbeat: null,
connectionUptime: Date.now(),
};
setInterval(() => this.reportHealth(), 60000); // Every minute
}
recordMessage(type, success = true) {
if (type === "sent") this.metrics.messagesSent++;
if (type === "received") this.metrics.messagesReceived++;
if (!success) this.metrics.errors++;
}
recordHeartbeat() {
this.metrics.lastHeartbeat = Date.now();
}
getHealthStatus() {
const now = Date.now();
const timeSinceHeartbeat = now - (this.metrics.lastHeartbeat || 0);
return {
status: timeSinceHeartbeat < 120000 ? "healthy" : "warning",
uptime: now - this.metrics.connectionUptime,
metrics: this.metrics,
lastHeartbeat: timeSinceHeartbeat,
};
}
reportHealth() {
const health = this.getHealthStatus();
console.log("π₯ Health Status:", health);
// Send to monitoring system
// sendToMonitoring(health);
}
}// Prometheus metrics for OCPP monitoring
const prometheus = require("prom-client");
// Custom metrics
const activeConnections = new prometheus.Gauge({
name: "ocpp_active_connections",
help: "Number of active WebSocket connections",
});
const messagesTotal = new prometheus.Counter({
name: "ocpp_messages_total",
help: "Total number of OCPP messages processed",
labelNames: ["action", "status", "charge_point_id"],
});
const messageProcessingDuration = new prometheus.Histogram({
name: "ocpp_message_processing_duration_seconds",
help: "Time spent processing OCPP messages",
labelNames: ["action"],
});
const transactionsActive = new prometheus.Gauge({
name: "ocpp_transactions_active",
help: "Number of active charging transactions",
});
// Usage in application
function recordMessage(action, status, chargePointId, duration) {
messagesTotal.inc({ action, status, charge_point_id: chargePointId });
messageProcessingDuration.observe({ action }, duration);
}
function updateActiveConnections(count) {
activeConnections.set(count);
}// Health check implementation
app.get('/health', (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
connections: getActiveConnectionCount(),
version: process.env.APP_VERSION || '1.0.0'
};
// Check database connectivity
const dbStatus = await checkDatabaseHealth();
health.database = dbStatus;
// Check Redis connectivity
const redisStatus = await checkRedisHealth();
health.redis = redisStatus;
const overallStatus = dbStatus.status === 'healthy' && redisStatus.status === 'healthy'
? 'healthy' : 'unhealthy';
res.status(overallStatus === 'healthy' ? 200 : 503).json(health);
});
app.get('/ready', (req, res) => {
// Readiness check for Kubernetes
const ready = {
status: 'ready',
services: {
database: await checkDatabaseHealth(),
redis: await checkRedisHealth(),
websocket: { status: 'healthy' }
}
};
const allReady = Object.values(ready.services)
.every(service => service.status === 'healthy');
res.status(allReady ? 200 : 503).json(ready);
});// Configuration management
const config = {
development: {
websocket: {
port: 8080,
timeout: 30000,
maxConnections: 100,
},
database: {
url:
process.env.DATABASE_URL ||
"postgresql://localhost:5432/ocpp_dev",
pool: { min: 2, max: 10 },
},
logging: {
level: "debug",
pretty: true,
},
},
production: {
websocket: {
port: process.env.PORT || 8080,
timeout: 60000,
maxConnections: 1000,
ssl: {
cert: process.env.SSL_CERT_PATH,
key: process.env.SSL_KEY_PATH,
},
},
database: {
url: process.env.DATABASE_URL,
pool: { min: 10, max: 50 },
ssl: { rejectUnauthorized: false },
},
logging: {
level: "info",
pretty: false,
file: "/app/logs/ocpp.log",
},
},
};
const currentConfig = config[process.env.NODE_ENV || "development"];This guide provides complete coverage of all OCPP 1.6 features:
- β Heartbeat, BootNotification, StatusNotification
- β StartTransaction, StopTransaction, MeterValues
- β Authorize, DataTransfer, FirmwareStatusNotification, DiagnosticsStatusNotification
- β Reset, UnlockConnector, RemoteStartTransaction, RemoteStopTransaction
- β ChangeConfiguration, GetConfiguration, GetDiagnostics, UpdateFirmware
- β ReserveNow, CancelReservation, ClearCache, GetLocalListVersion, SendLocalList
- β SetChargingProfile, ClearChargingProfile, GetCompositeSchedule
- β TriggerMessage, ChangeAvailability, DataTransfer
- β Profile 0 (No Security), Profile 1 (Basic Auth)
- β Profile 2 (Client Certificates), Profile 3 (Advanced)
- β Complete Node.js and Python implementations
- β Error handling and troubleshooting guides
- β Schema compliance and validation
- β Deployment and monitoring guidelines
- β Best practices and optimization tips
This guide equips you with everything needed to build robust, scalable OCPP 1.6 implementations that handle real-world charging scenarios, edge cases, and production requirements.
Happy Charging! β‘ππ