UI protocol: add OCPP heartbeat command support
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
CommitLineData
8114d10e 1import BaseError from '../../../exception/BaseError';
5a010bf0 2import { Bootstrap } from '../../../internal';
675fa8e3 3import {
32de5a57 4 ProcedureName,
675fa8e3
JB
5 ProtocolRequest,
6 ProtocolRequestHandler,
33cea517 7 ProtocolVersion,
89b7a234 8 RequestPayload,
32de5a57
LM
9 ResponsePayload,
10 ResponseStatus,
675fa8e3 11} from '../../../types/UIProtocol';
0d2cec76
JB
12import type {
13 BroadcastChannelProcedureName,
14 BroadcastChannelRequestPayload,
15} from '../../../types/WorkerBroadcastChannel';
675fa8e3 16import logger from '../../../utils/Logger';
0d2cec76 17import Utils from '../../../utils/Utils';
6c8f5d90 18import UIServiceWorkerBroadcastChannel from '../../UIServiceWorkerBroadcastChannel';
db2336d9 19import type { AbstractUIServer } from '../AbstractUIServer';
4198ad5c 20
32de5a57
LM
21const moduleName = 'AbstractUIService';
22
4198ad5c 23export default abstract class AbstractUIService {
02a6943a 24 protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>;
53870cff
JB
25 private readonly version: ProtocolVersion;
26 private readonly uiServer: AbstractUIServer;
27 private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel;
0d2cec76 28 private readonly broadcastChannelRequests: Map<string, number>;
4198ad5c 29
33cea517
JB
30 constructor(uiServer: AbstractUIServer, version: ProtocolVersion) {
31 this.version = version;
675fa8e3 32 this.uiServer = uiServer;
02a6943a 33 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
32de5a57
LM
34 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
35 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
36 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)],
4198ad5c 37 ]);
6812b4e1 38 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this);
0d2cec76 39 this.broadcastChannelRequests = new Map<string, number>();
4198ad5c
JB
40 }
41
5e3cb728 42 public async requestHandler(request: ProtocolRequest): Promise<void> {
98a5256a 43 let messageId: string;
32de5a57 44 let command: ProcedureName;
6c8f5d90 45 let requestPayload: RequestPayload | undefined;
32de5a57
LM
46 let responsePayload: ResponsePayload;
47 try {
5e3cb728 48 [messageId, command, requestPayload] = request;
32de5a57 49
02a6943a 50 if (this.requestHandlers.has(command) === false) {
32de5a57
LM
51 throw new BaseError(
52 `${command} is not implemented to handle message payload ${JSON.stringify(
53 requestPayload,
54 null,
55 2
56 )}`
57 );
4198ad5c 58 }
89b7a234 59
6c8f5d90 60 // Call the request handler to build the response payload
02a6943a 61 responsePayload = await this.requestHandlers.get(command)(messageId, requestPayload);
32de5a57
LM
62 } catch (error) {
63 // Log
1f7fa4de 64 logger.error(`${this.logPrefix(moduleName, 'messageHandler')} Handle request error:`, error);
6c8f5d90
JB
65 responsePayload = {
66 status: ResponseStatus.FAILURE,
67 command,
68 requestPayload,
69 responsePayload,
70 errorMessage: (error as Error).message,
71 errorStack: (error as Error).stack,
72 };
73 }
0d2cec76 74 // Send response for payload not forwarded to broadcast channel
6c8f5d90 75 if (responsePayload !== undefined) {
1f7fa4de 76 this.sendResponse(messageId ?? 'error', responsePayload);
4198ad5c 77 }
6c8f5d90
JB
78 }
79
80 public sendRequest(
81 messageId: string,
82 procedureName: ProcedureName,
83 requestPayload: RequestPayload
84 ): void {
852a4c5f
JB
85 this.uiServer.sendRequest(
86 this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
87 );
6c8f5d90 88 }
32de5a57 89
6c8f5d90 90 public sendResponse(messageId: string, responsePayload: ResponsePayload): void {
852a4c5f 91 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload));
4198ad5c
JB
92 }
93
6c8f5d90 94 public logPrefix(modName: string, methodName: string): string {
0d2cec76
JB
95 return this.uiServer.logPrefix(modName, methodName, this.version);
96 }
97
98 public deleteBroadcastChannelRequest(uuid: string): void {
99 this.broadcastChannelRequests.delete(uuid);
100 }
101
102 public getBroadcastChannelExpectedResponses(uuid: string): number {
103 return this.broadcastChannelRequests.get(uuid) ?? 0;
104 }
105
106 protected sendBroadcastChannelRequest(
107 uuid: string,
108 procedureName: BroadcastChannelProcedureName,
109 payload: BroadcastChannelRequestPayload
110 ): void {
111 if (!Utils.isEmptyArray(payload.hashIds)) {
112 payload.hashIds = payload.hashIds
113 .map((hashId) => {
114 if (this.uiServer.chargingStations.has(hashId) === true) {
115 return hashId;
116 }
117 logger.warn(
118 `${this.logPrefix(
119 moduleName,
120 'sendBroadcastChannelRequest'
121 )} Charging station with hashId '${hashId}' not found`
122 );
123 })
124 .filter((hashId) => hashId !== undefined);
125 }
126 const expectedNumberOfResponses = !Utils.isEmptyArray(payload.hashIds)
127 ? payload.hashIds.length
128 : this.uiServer.chargingStations.size;
0d2cec76 129 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload]);
853ed24a 130 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses);
6c8f5d90
JB
131 }
132
89b7a234 133 private handleListChargingStations(): ResponsePayload {
32de5a57
LM
134 return {
135 status: ResponseStatus.SUCCESS,
5e3cb728
JB
136 chargingStations: [...this.uiServer.chargingStations.values()],
137 } as ResponsePayload;
32de5a57
LM
138 }
139
140 private async handleStartSimulator(): Promise<ResponsePayload> {
141 await Bootstrap.getInstance().start();
142 return { status: ResponseStatus.SUCCESS };
143 }
144
145 private async handleStopSimulator(): Promise<ResponsePayload> {
146 await Bootstrap.getInstance().stop();
147 return { status: ResponseStatus.SUCCESS };
4198ad5c
JB
148 }
149}