UI protocol: add OCPP heartbeat command support
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationWorkerBroadcastChannel.ts
1 import BaseError from '../exception/BaseError';
2 import type OCPPError from '../exception/OCPPError';
3 import {
4 HeartbeatRequest,
5 RequestCommand,
6 type StatusNotificationRequest,
7 } from '../types/ocpp/Requests';
8 import type { HeartbeatResponse, StatusNotificationResponse } from '../types/ocpp/Responses';
9 import {
10 AuthorizationStatus,
11 StartTransactionRequest,
12 StartTransactionResponse,
13 StopTransactionRequest,
14 StopTransactionResponse,
15 } from '../types/ocpp/Transaction';
16 import {
17 BroadcastChannelProcedureName,
18 BroadcastChannelRequest,
19 BroadcastChannelRequestPayload,
20 BroadcastChannelResponsePayload,
21 MessageEvent,
22 } from '../types/WorkerBroadcastChannel';
23 import { ResponseStatus } from '../ui/web/src/types/UIProtocol';
24 import logger from '../utils/Logger';
25 import Utils from '../utils/Utils';
26 import type ChargingStation from './ChargingStation';
27 import WorkerBroadcastChannel from './WorkerBroadcastChannel';
28
29 const moduleName = 'ChargingStationWorkerBroadcastChannel';
30
31 type CommandResponse =
32 | StartTransactionResponse
33 | StopTransactionResponse
34 | StatusNotificationResponse
35 | HeartbeatResponse;
36
37 export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
38 private readonly chargingStation: ChargingStation;
39
40 constructor(chargingStation: ChargingStation) {
41 super();
42 this.chargingStation = chargingStation;
43 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
44 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
45 }
46
47 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
48 if (this.isResponse(messageEvent.data) === true) {
49 return;
50 }
51 const [uuid, command, requestPayload] = this.validateMessageEvent(messageEvent)
52 .data as BroadcastChannelRequest;
53
54 if (requestPayload?.hashIds !== undefined || requestPayload?.hashId !== undefined) {
55 if (
56 requestPayload?.hashId === undefined &&
57 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
58 ) {
59 return;
60 }
61 if (
62 requestPayload?.hashIds === undefined &&
63 requestPayload?.hashId !== this.chargingStation.stationInfo.hashId
64 ) {
65 return;
66 }
67 if (requestPayload?.hashId !== undefined) {
68 logger.warn(
69 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' instead`
70 );
71 }
72 }
73
74 let responsePayload: BroadcastChannelResponsePayload;
75 let commandResponse: CommandResponse;
76 try {
77 commandResponse = await this.commandHandler(command, requestPayload);
78 if (commandResponse === undefined) {
79 responsePayload = {
80 hashId: this.chargingStation.stationInfo.hashId,
81 status: ResponseStatus.SUCCESS,
82 };
83 } else {
84 responsePayload = {
85 hashId: this.chargingStation.stationInfo.hashId,
86 status: this.commandResponseToResponseStatus(command, commandResponse),
87 };
88 }
89 } catch (error) {
90 logger.error(
91 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
92 error
93 );
94 responsePayload = {
95 hashId: this.chargingStation.stationInfo.hashId,
96 status: ResponseStatus.FAILURE,
97 command,
98 requestPayload,
99 commandResponse,
100 errorMessage: (error as Error).message,
101 errorStack: (error as Error).stack,
102 errorDetails: (error as OCPPError).details,
103 };
104 }
105 this.sendResponse([uuid, responsePayload]);
106 }
107
108 private messageErrorHandler(messageEvent: MessageEvent): void {
109 logger.error(
110 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
111 { messageEvent }
112 );
113 }
114
115 private async commandHandler(
116 command: BroadcastChannelProcedureName,
117 requestPayload: BroadcastChannelRequestPayload
118 ): Promise<CommandResponse | undefined> {
119 switch (command) {
120 case BroadcastChannelProcedureName.START_CHARGING_STATION:
121 this.chargingStation.start();
122 break;
123 case BroadcastChannelProcedureName.STOP_CHARGING_STATION:
124 await this.chargingStation.stop();
125 break;
126 case BroadcastChannelProcedureName.OPEN_CONNECTION:
127 this.chargingStation.openWSConnection();
128 break;
129 case BroadcastChannelProcedureName.CLOSE_CONNECTION:
130 this.chargingStation.closeWSConnection();
131 break;
132 case BroadcastChannelProcedureName.START_TRANSACTION:
133 return this.chargingStation.ocppRequestService.requestHandler<
134 StartTransactionRequest,
135 StartTransactionResponse
136 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
137 connectorId: requestPayload.connectorId,
138 idTag: requestPayload.idTag,
139 });
140 case BroadcastChannelProcedureName.STOP_TRANSACTION:
141 return this.chargingStation.ocppRequestService.requestHandler<
142 StopTransactionRequest,
143 StopTransactionResponse
144 >(this.chargingStation, RequestCommand.STOP_TRANSACTION, {
145 transactionId: requestPayload.transactionId,
146 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
147 requestPayload.transactionId,
148 true
149 ),
150 idTag: requestPayload.idTag,
151 reason: requestPayload.reason,
152 });
153 case BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR:
154 this.chargingStation.startAutomaticTransactionGenerator(requestPayload.connectorIds);
155 break;
156 case BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR:
157 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload.connectorIds);
158 break;
159 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
160 return this.chargingStation.ocppRequestService.requestHandler<
161 StatusNotificationRequest,
162 StatusNotificationResponse
163 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, {
164 connectorId: requestPayload.connectorId,
165 errorCode: requestPayload.errorCode,
166 status: requestPayload.status,
167 ...(requestPayload.info && { info: requestPayload.info }),
168 ...(requestPayload.timestamp && { timestamp: requestPayload.timestamp }),
169 ...(requestPayload.vendorId && { vendorId: requestPayload.vendorId }),
170 ...(requestPayload.vendorErrorCode && {
171 vendorErrorCode: requestPayload.vendorErrorCode,
172 }),
173 });
174 case BroadcastChannelProcedureName.HEARTBEAT:
175 delete requestPayload.hashId;
176 delete requestPayload.hashIds;
177 delete requestPayload.connectorIds;
178 return this.chargingStation.ocppRequestService.requestHandler<
179 HeartbeatRequest,
180 HeartbeatResponse
181 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload);
182 default:
183 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
184 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
185 }
186 }
187
188 private commandResponseToResponseStatus(
189 command: BroadcastChannelProcedureName,
190 commandResponse: CommandResponse
191 ): ResponseStatus {
192 switch (command) {
193 case BroadcastChannelProcedureName.START_TRANSACTION:
194 case BroadcastChannelProcedureName.STOP_TRANSACTION:
195 if (
196 (commandResponse as StartTransactionResponse | StopTransactionResponse)?.idTagInfo
197 ?.status === AuthorizationStatus.ACCEPTED
198 ) {
199 return ResponseStatus.SUCCESS;
200 }
201 return ResponseStatus.FAILURE;
202 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
203 if (Utils.isEmptyObject(commandResponse) === true) {
204 return ResponseStatus.SUCCESS;
205 }
206 return ResponseStatus.FAILURE;
207 case BroadcastChannelProcedureName.HEARTBEAT:
208 if ('currentTime' in commandResponse) {
209 return ResponseStatus.SUCCESS;
210 }
211 return ResponseStatus.FAILURE;
212 default:
213 return ResponseStatus.FAILURE;
214 }
215 }
216 }