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