Add initial support for OCPP 1.6 firmware update simulation
[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 { StandardParametersKey } from '../types/ocpp/Configuration';
4 import {
5 type BootNotificationRequest,
6 type DataTransferRequest,
7 type DiagnosticsStatusNotificationRequest,
8 type FirmwareStatusNotificationRequest,
9 type HeartbeatRequest,
10 type MeterValuesRequest,
11 RequestCommand,
12 type StatusNotificationRequest,
13 } from '../types/ocpp/Requests';
14 import {
15 type BootNotificationResponse,
16 type DataTransferResponse,
17 DataTransferStatus,
18 type DiagnosticsStatusNotificationResponse,
19 type FirmwareStatusNotificationResponse,
20 type HeartbeatResponse,
21 type MeterValuesResponse,
22 RegistrationStatusEnumType,
23 type StatusNotificationResponse,
24 } from '../types/ocpp/Responses';
25 import {
26 AuthorizationStatus,
27 type AuthorizeRequest,
28 type AuthorizeResponse,
29 type StartTransactionRequest,
30 type StartTransactionResponse,
31 type StopTransactionRequest,
32 type StopTransactionResponse,
33 } from '../types/ocpp/Transaction';
34 import { ResponseStatus } from '../types/UIProtocol';
35 import {
36 BroadcastChannelProcedureName,
37 type BroadcastChannelRequest,
38 type BroadcastChannelRequestPayload,
39 type BroadcastChannelResponsePayload,
40 type MessageEvent,
41 } from '../types/WorkerBroadcastChannel';
42 import Constants from '../utils/Constants';
43 import logger from '../utils/Logger';
44 import Utils from '../utils/Utils';
45 import type ChargingStation from './ChargingStation';
46 import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
47 import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils';
48 import WorkerBroadcastChannel from './WorkerBroadcastChannel';
49
50 const moduleName = 'ChargingStationWorkerBroadcastChannel';
51
52 type CommandResponse =
53 | StartTransactionResponse
54 | StopTransactionResponse
55 | AuthorizeResponse
56 | BootNotificationResponse
57 | StatusNotificationResponse
58 | HeartbeatResponse
59 | MeterValuesResponse
60 | DataTransferResponse
61 | DiagnosticsStatusNotificationResponse
62 | FirmwareStatusNotificationResponse;
63
64 type CommandHandler = (
65 requestPayload?: BroadcastChannelRequestPayload
66 ) => Promise<CommandResponse | void> | void;
67
68 export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
69 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
70 private readonly chargingStation: ChargingStation;
71
72 constructor(chargingStation: ChargingStation) {
73 super();
74 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
75 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
76 [
77 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
78 async () => this.chargingStation.stop(),
79 ],
80 [
81 BroadcastChannelProcedureName.OPEN_CONNECTION,
82 () => this.chargingStation.openWSConnection(),
83 ],
84 [
85 BroadcastChannelProcedureName.CLOSE_CONNECTION,
86 () => this.chargingStation.closeWSConnection(),
87 ],
88 [
89 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
90 (requestPayload?: BroadcastChannelRequestPayload) =>
91 this.chargingStation.startAutomaticTransactionGenerator(requestPayload.connectorIds),
92 ],
93 [
94 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
95 (requestPayload?: BroadcastChannelRequestPayload) =>
96 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload.connectorIds),
97 ],
98 [
99 BroadcastChannelProcedureName.START_TRANSACTION,
100 async (requestPayload?: BroadcastChannelRequestPayload) =>
101 this.chargingStation.ocppRequestService.requestHandler<
102 StartTransactionRequest,
103 StartTransactionResponse
104 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload),
105 ],
106 [
107 BroadcastChannelProcedureName.STOP_TRANSACTION,
108 async (requestPayload?: BroadcastChannelRequestPayload) =>
109 this.chargingStation.ocppRequestService.requestHandler<
110 StopTransactionRequest,
111 StartTransactionResponse
112 >(this.chargingStation, RequestCommand.STOP_TRANSACTION, {
113 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
114 requestPayload.transactionId,
115 true
116 ),
117 ...requestPayload,
118 }),
119 ],
120 [
121 BroadcastChannelProcedureName.AUTHORIZE,
122 async (requestPayload?: BroadcastChannelRequestPayload) =>
123 this.chargingStation.ocppRequestService.requestHandler<
124 AuthorizeRequest,
125 AuthorizeResponse
126 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload),
127 ],
128 [
129 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
130 async (requestPayload?: BroadcastChannelRequestPayload) => {
131 this.chargingStation.bootNotificationResponse =
132 await this.chargingStation.ocppRequestService.requestHandler<
133 BootNotificationRequest,
134 BootNotificationResponse
135 >(
136 this.chargingStation,
137 RequestCommand.BOOT_NOTIFICATION,
138 {
139 ...this.chargingStation.bootNotificationRequest,
140 ...requestPayload,
141 },
142 {
143 skipBufferingOnError: true,
144 }
145 );
146 return this.chargingStation.bootNotificationResponse;
147 },
148 ],
149 [
150 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
151 async (requestPayload?: BroadcastChannelRequestPayload) =>
152 this.chargingStation.ocppRequestService.requestHandler<
153 StatusNotificationRequest,
154 StatusNotificationResponse
155 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload),
156 ],
157 [
158 BroadcastChannelProcedureName.HEARTBEAT,
159 async (requestPayload?: BroadcastChannelRequestPayload) =>
160 this.chargingStation.ocppRequestService.requestHandler<
161 HeartbeatRequest,
162 HeartbeatResponse
163 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload),
164 ],
165 [
166 BroadcastChannelProcedureName.METER_VALUES,
167 async (requestPayload?: BroadcastChannelRequestPayload) => {
168 const configuredMeterValueSampleInterval =
169 ChargingStationConfigurationUtils.getConfigurationKey(
170 chargingStation,
171 StandardParametersKey.MeterValueSampleInterval
172 );
173 return this.chargingStation.ocppRequestService.requestHandler<
174 MeterValuesRequest,
175 MeterValuesResponse
176 >(this.chargingStation, RequestCommand.METER_VALUES, {
177 meterValue: [
178 OCPP16ServiceUtils.buildMeterValue(
179 this.chargingStation,
180 requestPayload.connectorId,
181 this.chargingStation.getConnectorStatus(requestPayload.connectorId)?.transactionId,
182 configuredMeterValueSampleInterval
183 ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000
184 : Constants.DEFAULT_METER_VALUES_INTERVAL
185 ),
186 ],
187 ...requestPayload,
188 });
189 },
190 ],
191 [
192 BroadcastChannelProcedureName.DATA_TRANSFER,
193 async (requestPayload?: BroadcastChannelRequestPayload) =>
194 this.chargingStation.ocppRequestService.requestHandler<
195 DataTransferRequest,
196 DataTransferResponse
197 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload),
198 ],
199 [
200 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
201 async (requestPayload?: BroadcastChannelRequestPayload) =>
202 this.chargingStation.ocppRequestService.requestHandler<
203 DiagnosticsStatusNotificationRequest,
204 DiagnosticsStatusNotificationResponse
205 >(this.chargingStation, RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, requestPayload),
206 ],
207 [
208 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
209 async (requestPayload?: BroadcastChannelRequestPayload) =>
210 this.chargingStation.ocppRequestService.requestHandler<
211 FirmwareStatusNotificationRequest,
212 FirmwareStatusNotificationResponse
213 >(this.chargingStation, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, requestPayload),
214 ],
215 ]);
216 this.chargingStation = chargingStation;
217 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
218 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
219 }
220
221 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
222 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
223 if (validatedMessageEvent === false) {
224 return;
225 }
226 if (this.isResponse(validatedMessageEvent.data) === true) {
227 return;
228 }
229 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
230 if (
231 requestPayload?.hashIds !== undefined &&
232 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
233 ) {
234 return;
235 }
236 if (requestPayload?.hashId !== undefined) {
237 logger.error(
238 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
239 );
240 return;
241 }
242 let responsePayload: BroadcastChannelResponsePayload;
243 let commandResponse: CommandResponse | void;
244 try {
245 commandResponse = await this.commandHandler(command, requestPayload);
246 if (
247 commandResponse === undefined ||
248 commandResponse === null ||
249 Utils.isEmptyObject(commandResponse as CommandResponse)
250 ) {
251 responsePayload = {
252 hashId: this.chargingStation.stationInfo.hashId,
253 status: ResponseStatus.SUCCESS,
254 };
255 } else {
256 responsePayload = this.commandResponseToResponsePayload(
257 command,
258 requestPayload,
259 commandResponse as CommandResponse
260 );
261 }
262 } catch (error) {
263 logger.error(
264 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
265 error
266 );
267 responsePayload = {
268 hashId: this.chargingStation.stationInfo.hashId,
269 status: ResponseStatus.FAILURE,
270 command,
271 requestPayload,
272 commandResponse: commandResponse as CommandResponse,
273 errorMessage: (error as Error).message,
274 errorStack: (error as Error).stack,
275 errorDetails: (error as OCPPError).details,
276 };
277 } finally {
278 this.sendResponse([uuid, responsePayload]);
279 }
280 }
281
282 private messageErrorHandler(messageEvent: MessageEvent): void {
283 logger.error(
284 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
285 messageEvent
286 );
287 }
288
289 private async commandHandler(
290 command: BroadcastChannelProcedureName,
291 requestPayload: BroadcastChannelRequestPayload
292 ): Promise<CommandResponse | void> {
293 if (this.commandHandlers.has(command) === true) {
294 this.cleanRequestPayload(command, requestPayload);
295 return this.commandHandlers.get(command)(requestPayload);
296 }
297 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
298 }
299
300 private cleanRequestPayload(
301 command: BroadcastChannelProcedureName,
302 requestPayload: BroadcastChannelRequestPayload
303 ): void {
304 delete requestPayload.hashId;
305 delete requestPayload.hashIds;
306 [
307 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
308 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
309 ].includes(command) === false && delete requestPayload.connectorIds;
310 }
311
312 private commandResponseToResponsePayload(
313 command: BroadcastChannelProcedureName,
314 requestPayload: BroadcastChannelRequestPayload,
315 commandResponse: CommandResponse
316 ): BroadcastChannelResponsePayload {
317 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
318 if (responseStatus === ResponseStatus.SUCCESS) {
319 return {
320 hashId: this.chargingStation.stationInfo.hashId,
321 status: responseStatus,
322 };
323 }
324 return {
325 hashId: this.chargingStation.stationInfo.hashId,
326 status: responseStatus,
327 command,
328 requestPayload,
329 commandResponse,
330 };
331 }
332
333 private commandResponseToResponseStatus(
334 command: BroadcastChannelProcedureName,
335 commandResponse: CommandResponse
336 ): ResponseStatus {
337 switch (command) {
338 case BroadcastChannelProcedureName.START_TRANSACTION:
339 case BroadcastChannelProcedureName.STOP_TRANSACTION:
340 case BroadcastChannelProcedureName.AUTHORIZE:
341 if (
342 (
343 commandResponse as
344 | StartTransactionResponse
345 | StopTransactionResponse
346 | AuthorizeResponse
347 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
348 ) {
349 return ResponseStatus.SUCCESS;
350 }
351 return ResponseStatus.FAILURE;
352 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
353 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
354 return ResponseStatus.SUCCESS;
355 }
356 return ResponseStatus.FAILURE;
357 case BroadcastChannelProcedureName.DATA_TRANSFER:
358 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
359 return ResponseStatus.SUCCESS;
360 }
361 return ResponseStatus.FAILURE;
362 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
363 case BroadcastChannelProcedureName.METER_VALUES:
364 if (Utils.isEmptyObject(commandResponse) === true) {
365 return ResponseStatus.SUCCESS;
366 }
367 return ResponseStatus.FAILURE;
368 case BroadcastChannelProcedureName.HEARTBEAT:
369 if ('currentTime' in commandResponse) {
370 return ResponseStatus.SUCCESS;
371 }
372 return ResponseStatus.FAILURE;
373 default:
374 return ResponseStatus.FAILURE;
375 }
376 }
377 }