UI protocol: add OCPP heartbeat command support
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationWorkerBroadcastChannel.ts
CommitLineData
6c8f5d90 1import BaseError from '../exception/BaseError';
a9ed42b2 2import type OCPPError from '../exception/OCPPError';
10db00b2
JB
3import {
4 HeartbeatRequest,
5 RequestCommand,
6 type StatusNotificationRequest,
7} from '../types/ocpp/Requests';
8import type { HeartbeatResponse, StatusNotificationResponse } from '../types/ocpp/Responses';
89b7a234 9import {
6c8f5d90 10 AuthorizationStatus,
89b7a234
JB
11 StartTransactionRequest,
12 StartTransactionResponse,
89b7a234
JB
13 StopTransactionRequest,
14 StopTransactionResponse,
15} from '../types/ocpp/Transaction';
16import {
17 BroadcastChannelProcedureName,
18 BroadcastChannelRequest,
6c8f5d90
JB
19 BroadcastChannelRequestPayload,
20 BroadcastChannelResponsePayload,
21 MessageEvent,
89b7a234 22} from '../types/WorkerBroadcastChannel';
f27eb751 23import { ResponseStatus } from '../ui/web/src/types/UIProtocol';
6c8f5d90 24import logger from '../utils/Logger';
a9ed42b2 25import Utils from '../utils/Utils';
db2336d9 26import type ChargingStation from './ChargingStation';
1598b27c 27import WorkerBroadcastChannel from './WorkerBroadcastChannel';
89b7a234 28
4e3ff94d
JB
29const moduleName = 'ChargingStationWorkerBroadcastChannel';
30
a9ed42b2
JB
31type CommandResponse =
32 | StartTransactionResponse
33 | StopTransactionResponse
10db00b2
JB
34 | StatusNotificationResponse
35 | HeartbeatResponse;
89b7a234 36
1598b27c 37export default class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
89b7a234
JB
38 private readonly chargingStation: ChargingStation;
39
40 constructor(chargingStation: ChargingStation) {
1598b27c 41 super();
89b7a234 42 this.chargingStation = chargingStation;
02a6943a 43 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
6c8f5d90 44 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
89b7a234
JB
45 }
46
02a6943a 47 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
3880dbcf 48 if (this.isResponse(messageEvent.data) === true) {
6c8f5d90
JB
49 return;
50 }
5e3cb728
JB
51 const [uuid, command, requestPayload] = this.validateMessageEvent(messageEvent)
52 .data as BroadcastChannelRequest;
6c8f5d90 53
18057587
JB
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 }
4eca248c 72 }
89b7a234 73
6c8f5d90
JB
74 let responsePayload: BroadcastChannelResponsePayload;
75 let commandResponse: CommandResponse;
76 try {
77 commandResponse = await this.commandHandler(command, requestPayload);
78 if (commandResponse === undefined) {
10d244c0 79 responsePayload = {
51c83d6f 80 hashId: this.chargingStation.stationInfo.hashId,
10d244c0
JB
81 status: ResponseStatus.SUCCESS,
82 };
6c8f5d90 83 } else {
10d244c0 84 responsePayload = {
51c83d6f 85 hashId: this.chargingStation.stationInfo.hashId,
10db00b2 86 status: this.commandResponseToResponseStatus(command, commandResponse),
10d244c0 87 };
6c8f5d90
JB
88 }
89 } catch (error) {
90 logger.error(
91 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
92 error
93 );
94 responsePayload = {
51c83d6f 95 hashId: this.chargingStation.stationInfo.hashId,
6c8f5d90
JB
96 status: ResponseStatus.FAILURE,
97 command,
98 requestPayload,
99 commandResponse,
100 errorMessage: (error as Error).message,
101 errorStack: (error as Error).stack,
a9ed42b2 102 errorDetails: (error as OCPPError).details,
6c8f5d90
JB
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:`,
5e3cb728 111 { messageEvent }
6c8f5d90
JB
112 );
113 }
114
115 private async commandHandler(
116 command: BroadcastChannelProcedureName,
117 requestPayload: BroadcastChannelRequestPayload
118 ): Promise<CommandResponse | undefined> {
89b7a234 119 switch (command) {
4f69be04
JB
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;
89b7a234 132 case BroadcastChannelProcedureName.START_TRANSACTION:
6c8f5d90 133 return this.chargingStation.ocppRequestService.requestHandler<
89b7a234
JB
134 StartTransactionRequest,
135 StartTransactionResponse
136 >(this.chargingStation, RequestCommand.START_TRANSACTION, {
6c8f5d90
JB
137 connectorId: requestPayload.connectorId,
138 idTag: requestPayload.idTag,
89b7a234 139 });
89b7a234 140 case BroadcastChannelProcedureName.STOP_TRANSACTION:
6c8f5d90 141 return this.chargingStation.ocppRequestService.requestHandler<
89b7a234
JB
142 StopTransactionRequest,
143 StopTransactionResponse
144 >(this.chargingStation, RequestCommand.STOP_TRANSACTION, {
6c8f5d90 145 transactionId: requestPayload.transactionId,
89b7a234 146 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
07989fad
JB
147 requestPayload.transactionId,
148 true
89b7a234 149 ),
cf058664
JB
150 idTag: requestPayload.idTag,
151 reason: requestPayload.reason,
89b7a234 152 });
4f69be04 153 case BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR:
a5e9befc 154 this.chargingStation.startAutomaticTransactionGenerator(requestPayload.connectorIds);
89b7a234 155 break;
4f69be04 156 case BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR:
a5e9befc 157 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload.connectorIds);
db2336d9 158 break;
a9ed42b2
JB
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 });
10db00b2
JB
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);
6c8f5d90
JB
182 default:
183 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ce7a4fc3 184 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
6c8f5d90
JB
185 }
186 }
187
10db00b2
JB
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;
89b7a234
JB
214 }
215 }
216}