UI protocol: add OCPP heartbeat command support
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 5 Sep 2022 20:51:12 +0000 (22:51 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 5 Sep 2022 20:51:12 +0000 (22:51 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
README.md
src/assets/Insomnia_CSSimulatorUIProtocol.json
src/charging-station/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/ui-server/ui-services/UIService001.ts
src/types/UIProtocol.ts
src/types/WorkerBroadcastChannel.ts

index eee5d343b2c24122cf9f86ed401ad19becd0dfa6..0611fd3b183db229dcbb54951d14ef1f35a0bd2d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -584,7 +584,25 @@ Set the Websocket header _Sec-Websocket-Protocol_ to `ui0.0.1`.
   `ProcedureName`: 'StatusNotification'  
   `PDU`: {  
   `hashIds`: charging station unique identifier strings array (optional, default: all charging stations),  
-  `connectorId`: connector id integer  
+  `connectorId`: connector id integer,  
+  `errorCode`: connector error code,  
+  `status`: connector status  
+  }
+
+- Response:  
+  `PDU`: {  
+  `status`: 'success' | 'failure',  
+  `hashIdsSucceeded`: charging station unique identifier strings array,  
+  `hashIdsFailed`: charging station unique identifier strings array (optional),  
+  `responsesFailed`: failed responses payload array (optional)  
+  }
+
+###### Heartbeat
+
+- Request:  
+  `ProcedureName`: 'Heartbeat'  
+  `PDU`: {  
+  `hashIds`: charging station unique identifier strings array (optional, default: all charging stations),  
   }
 
 - Response:  
index ddd79dcaba5c26a1180ac4822c21ba340a9db391..61dded0312a687c82188141ce5c15ce250fe39f6 100644 (file)
@@ -1,13 +1,13 @@
 {
   "_type": "export",
   "__export_format": 4,
-  "__export_date": "2022-09-05T08:32:56.599Z",
+  "__export_date": "2022-09-05T20:42:59.578Z",
   "__export_source": "insomnia.desktop.app:v2022.5.1",
   "resources": [
     {
       "_id": "req_606dcee139984772877def40fcbb5c76",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662340741508,
+      "modified": 1662410526425,
       "created": 1661789624987,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/listChargingStations",
       "name": "listChargingStations",
@@ -52,7 +52,7 @@
     {
       "_id": "req_7d5f9506e7ac49208a4f960a7740663e",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662312719968,
+      "modified": 1662373634204,
       "created": 1661789624990,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/startSimulator",
       "name": "startSimulator",
@@ -87,7 +87,7 @@
     {
       "_id": "req_59056be11534481c80a0b0da32e2a06a",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662302216944,
+      "modified": 1662367707319,
       "created": 1661789624994,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/stopSimulator",
       "name": "stopSimulator",
     {
       "_id": "req_aad7fd6db4c64869b60048b915010efc",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662340686080,
+      "modified": 1662372497848,
       "created": 1661789624998,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/startChargingStation",
       "name": "startChargingStation",
     {
       "_id": "req_d72d91cf3fb044179b8ae9d92a74f99c",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662308938677,
+      "modified": 1662372491437,
       "created": 1661789625002,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/stopChargingStation",
       "name": "stopChargingStation",
     {
       "_id": "req_747f458d196f4681b5fe15204b0067aa",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662340696576,
+      "modified": 1662367716472,
       "created": 1661789625005,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/openConnection",
       "name": "openConnection",
     {
       "_id": "req_401e6a62a33c4b6c90aaa2e019daab6d",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662340695169,
+      "modified": 1662367720232,
       "created": 1661789625014,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/closeConnection",
       "name": "closeConnection",
     {
       "_id": "req_2f757efe92fb4936ad4fa4b6763f9293",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662340693347,
+      "modified": 1662367718288,
       "created": 1661789625017,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/startTransaction",
       "name": "startTransaction",
     {
       "_id": "req_7c285fb6cb6948a08235a6c73cbeb1f9",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662340705489,
+      "modified": 1662367694077,
       "created": 1661789625020,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/stopTransaction",
       "name": "stopTransaction",
     {
       "_id": "req_b33c704fe3464dc5a5d3694abd9320d0",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662366450174,
+      "modified": 1662372502338,
       "created": 1661803778569,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/startAutomaticTransactionGenerator",
       "name": "startAutomaticTransactionGenerator",
     {
       "_id": "req_24c1c55fe3ba4ddb94702408f21a64df",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662366450665,
+      "modified": 1662372479288,
       "created": 1661803846882,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/stopAutomaticTransactionGenerator",
       "name": "stopAutomaticTransactionGenerator",
     {
       "_id": "req_6a78267706094fb59d85ed1531e07a55",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
-      "modified": 1662366453452,
+      "modified": 1662410523458,
       "created": 1662330215407,
       "url": "{{baseUrl}}/{{protocol}}/{{version}}/statusNotification",
       "name": "statusNotification",
       "method": "POST",
       "body": {
         "mimeType": "application/json",
-        "text": "{\n\t\"hashIds\": [\n\t\t\"0058d8b50e422cce5bbd0c0a4ad13d5d657e8a88670dcf04c1b2b563fea3db5b96a3686278b374ed050e21baef89060e\",\n\t\t\"331d024fea36e3e2483a0e5dc9376234241c8c099ad201a441437b23622c308555183f37cbc84a1818c1c45aaae50896\"\n\t],\n\t\"connectorId\": 1,\n\t\"errorCode\": \"NoError\",\n\t\"status\": \"Preparing\"\n}"
+        "text": "{\n\t\"hashIds\": [\n\t\t\"0058d8b50e422cce5bbd0c0a4ad13d5d657e8a88670dcf04c1b2b563fea3db5b96a3686278b374ed050e21baef89060e\",\n\t\t\"331d024fea36e3e2483a0e5dc9376234241c8c099ad201a441437b23622c308555183f37cbc84a1818c1c45aaae50896\"\n\t],\n\t\"connectorId\": 1,\n\t\"errorCode\": \"NoError\",\n\t\"status\": \"Available\"\n}"
       },
       "parameters": [],
       "headers": [
       "settingFollowRedirects": "global",
       "_type": "request"
     },
+    {
+      "_id": "req_61efafe9f4a14c268b948b9f9c5c4195",
+      "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
+      "modified": 1662410531123,
+      "created": 1662409405256,
+      "url": "{{baseUrl}}/{{protocol}}/{{version}}/heartbeat",
+      "name": "heartbeat",
+      "description": "",
+      "method": "POST",
+      "body": {
+        "mimeType": "application/json",
+        "text": "{\n\t\"hashIds\": [\n\t\t\"0058d8b50e422cce5bbd0c0a4ad13d5d657e8a88670dcf04c1b2b563fea3db5b96a3686278b374ed050e21baef89060e\",\n\t\t\"331d024fea36e3e2483a0e5dc9376234241c8c099ad201a441437b23622c308555183f37cbc84a1818c1c45aaae50896\"\n\t]\n}"
+      },
+      "parameters": [],
+      "headers": [
+        {
+          "name": "Content-Type",
+          "value": "application/json",
+          "id": "pair_3224616dd6604605a1e48b71f6e9f795"
+        }
+      ],
+      "authentication": {
+        "type": "basic",
+        "useISO88591": false,
+        "disabled": false,
+        "username": "{{username}}",
+        "password": "{{password}}"
+      },
+      "metaSortKey": -999999500,
+      "isPrivate": false,
+      "settingStoreCookies": true,
+      "settingSendCookies": true,
+      "settingDisableRenderRequestBody": false,
+      "settingEncodeUrl": true,
+      "settingRebuildPath": true,
+      "settingFollowRedirects": "global",
+      "_type": "request"
+    },
     {
       "_id": "env_74b29d59b9f04298b97fc9750476a4ca",
       "parentId": "wrk_d64b10b1e0c14563a80484ee684b5205",
index dfb50ab1dd67d318f251ed88a953aae67011e276..81996076211150a75e9c7a9c597a1628a8250916 100644 (file)
@@ -1,7 +1,11 @@
 import BaseError from '../exception/BaseError';
 import type OCPPError from '../exception/OCPPError';
-import { RequestCommand, type StatusNotificationRequest } from '../types/ocpp/Requests';
-import type { StatusNotificationResponse } from '../types/ocpp/Responses';
+import {
+  HeartbeatRequest,
+  RequestCommand,
+  type StatusNotificationRequest,
+} from '../types/ocpp/Requests';
+import type { HeartbeatResponse, StatusNotificationResponse } from '../types/ocpp/Responses';
 import {
   AuthorizationStatus,
   StartTransactionRequest,
@@ -27,7 +31,8 @@ const moduleName = 'ChargingStationWorkerBroadcastChannel';
 type CommandResponse =
   | StartTransactionResponse
   | StopTransactionResponse
-  | StatusNotificationResponse;
+  | StatusNotificationResponse
+  | HeartbeatResponse;
 
 export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
   private readonly chargingStation: ChargingStation;
@@ -78,7 +83,7 @@ export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadca
       } else {
         responsePayload = {
           hashId: this.chargingStation.stationInfo.hashId,
-          status: this.commandResponseToResponseStatus(commandResponse),
+          status: this.commandResponseToResponseStatus(command, commandResponse),
         };
       }
     } catch (error) {
@@ -166,19 +171,46 @@ export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadca
             vendorErrorCode: requestPayload.vendorErrorCode,
           }),
         });
+      case BroadcastChannelProcedureName.HEARTBEAT:
+        delete requestPayload.hashId;
+        delete requestPayload.hashIds;
+        delete requestPayload.connectorIds;
+        return this.chargingStation.ocppRequestService.requestHandler<
+          HeartbeatRequest,
+          HeartbeatResponse
+        >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload);
       default:
         // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
         throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
     }
   }
 
-  private commandResponseToResponseStatus(commandResponse: CommandResponse): ResponseStatus {
-    if (
-      Utils.isEmptyObject(commandResponse) ||
-      commandResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
-    ) {
-      return ResponseStatus.SUCCESS;
+  private commandResponseToResponseStatus(
+    command: BroadcastChannelProcedureName,
+    commandResponse: CommandResponse
+  ): ResponseStatus {
+    switch (command) {
+      case BroadcastChannelProcedureName.START_TRANSACTION:
+      case BroadcastChannelProcedureName.STOP_TRANSACTION:
+        if (
+          (commandResponse as StartTransactionResponse | StopTransactionResponse)?.idTagInfo
+            ?.status === AuthorizationStatus.ACCEPTED
+        ) {
+          return ResponseStatus.SUCCESS;
+        }
+        return ResponseStatus.FAILURE;
+      case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
+        if (Utils.isEmptyObject(commandResponse) === true) {
+          return ResponseStatus.SUCCESS;
+        }
+        return ResponseStatus.FAILURE;
+      case BroadcastChannelProcedureName.HEARTBEAT:
+        if ('currentTime' in commandResponse) {
+          return ResponseStatus.SUCCESS;
+        }
+        return ResponseStatus.FAILURE;
+      default:
+        return ResponseStatus.FAILURE;
     }
-    return ResponseStatus.FAILURE;
   }
 }
index 74403ceaaed5cbbbca6b99bc4b740f1da13519b6..57c34c2239a6ee573d7462d5e57c96b1157f2cfc 100644 (file)
@@ -47,6 +47,10 @@ export default class UIService001 extends AbstractUIService {
       ProcedureName.STATUS_NOTIFICATION,
       this.handleStatusNotification.bind(this) as ProtocolRequestHandler
     );
+    this.requestHandlers.set(
+      ProcedureName.HEARTBEAT,
+      this.handleHeartbeat.bind(this) as ProtocolRequestHandler
+    );
   }
 
   private handleStartChargingStation(uuid: string, payload: RequestPayload): void {
@@ -108,4 +112,8 @@ export default class UIService001 extends AbstractUIService {
       payload
     );
   }
+
+  private handleHeartbeat(uuid: string, payload: RequestPayload): void {
+    this.sendBroadcastChannelRequest(uuid, BroadcastChannelProcedureName.HEARTBEAT, payload);
+  }
 }
index d995e096676fd7d40a3944defb8cab8d9f91fb2d..4b96262bc3126afaf3d6ad8693758b9f09f23dfa 100644 (file)
@@ -39,6 +39,7 @@ export enum ProcedureName {
   START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
   STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
   STATUS_NOTIFICATION = 'statusNotification',
+  HEARTBEAT = 'heartbeat',
 }
 
 export interface RequestPayload extends JsonObject {
index f235200c283b0393e900754b19e93d9e26f377fc..788f7d68f864c63f3dce8f88e558d3fc6c94e496 100644 (file)
@@ -17,6 +17,7 @@ export enum BroadcastChannelProcedureName {
   START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
   STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
   STATUS_NOTIFICATION = 'statusNotification',
+  HEARTBEAT = 'heartbeat',
 }
 
 export interface BroadcastChannelRequestPayload extends RequestPayload {