fix(simulator): order firmware upgrade statuses properly
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
b4d34251 2
43ee4373 3import crypto from 'node:crypto';
130783a7
JB
4import fs from 'node:fs';
5import path from 'node:path';
6import { URL } from 'node:url';
01f4001e 7import { parentPort } from 'node:worker_threads';
8114d10e 8
5d280aae 9import merge from 'just-merge';
ef7d8c21 10import WebSocket, { type RawData } from 'ws';
8114d10e 11
368a6eda 12import {
2896e06d
JB
13 AuthorizedTagsCache,
14 AutomaticTransactionGenerator,
15 ChargingStationConfigurationUtils,
16 ChargingStationUtils,
17 ChargingStationWorkerBroadcastChannel,
18 MessageChannelUtils,
19 SharedLRUCache,
20} from './internal';
21import {
22 // OCPP16IncomingRequestService,
368a6eda 23 OCPP16RequestService,
2896e06d 24 // OCPP16ResponseService,
368a6eda
JB
25 OCPP16ServiceUtils,
26 OCPP20IncomingRequestService,
27 OCPP20RequestService,
2896e06d 28 // OCPP20ResponseService,
368a6eda
JB
29 type OCPPIncomingRequestService,
30 type OCPPRequestService,
2896e06d 31 // OCPPServiceUtils,
368a6eda 32} from './ocpp';
2896e06d
JB
33import { OCPP16IncomingRequestService } from './ocpp/1.6/OCPP16IncomingRequestService';
34import { OCPP16ResponseService } from './ocpp/1.6/OCPP16ResponseService';
35import { OCPP20ResponseService } from './ocpp/2.0/OCPP20ResponseService';
36import { OCPPServiceUtils } from './ocpp/OCPPServiceUtils';
268a74bb 37import { BaseError, OCPPError } from '../exception';
dada83ec 38import { PerformanceStatistics } from '../performance';
e7aeea18 39import {
268a74bb 40 type AutomaticTransactionGeneratorConfiguration,
e7aeea18 41 AvailabilityType,
e0b0ee21 42 type BootNotificationRequest,
268a74bb 43 type BootNotificationResponse,
e0b0ee21 44 type CachedRequest,
268a74bb
JB
45 type ChargingStationConfiguration,
46 type ChargingStationInfo,
47 type ChargingStationOcppConfiguration,
48 type ChargingStationTemplate,
49 ConnectorPhaseRotation,
50 ConnectorStatus,
51 ConnectorStatusEnum,
52 CurrentType,
e0b0ee21 53 type ErrorCallback,
268a74bb
JB
54 type ErrorResponse,
55 ErrorType,
56 FileType,
c9a4f9ea
JB
57 FirmwareStatus,
58 type FirmwareStatusNotificationRequest,
268a74bb
JB
59 type FirmwareStatusNotificationResponse,
60 type FirmwareUpgrade,
e0b0ee21 61 type HeartbeatRequest,
268a74bb 62 type HeartbeatResponse,
e0b0ee21 63 type IncomingRequest,
268a74bb
JB
64 type IncomingRequestCommand,
65 type JsonType,
66 MessageType,
67 type MeterValue,
68 MeterValueMeasurand,
e0b0ee21 69 type MeterValuesRequest,
268a74bb
JB
70 type MeterValuesResponse,
71 OCPPVersion,
8ca6874c 72 type OutgoingRequest,
268a74bb
JB
73 PowerUnits,
74 RegistrationStatusEnumType,
e7aeea18 75 RequestCommand,
268a74bb 76 type Response,
e0b0ee21 77 type ResponseCallback,
268a74bb 78 StandardParametersKey,
e0b0ee21 79 type StatusNotificationRequest,
e0b0ee21 80 type StatusNotificationResponse,
ef6fa3fb 81 StopTransactionReason,
e0b0ee21
JB
82 type StopTransactionRequest,
83 type StopTransactionResponse,
268a74bb
JB
84 SupervisionUrlDistribution,
85 SupportedFeatureProfiles,
6dad8e21 86 VendorParametersKey,
268a74bb
JB
87 type WSError,
88 WebSocketCloseEventStatusCode,
89 type WsOptions,
90} from '../types';
60a74391
JB
91import {
92 ACElectricUtils,
93 Configuration,
94 Constants,
95 DCElectricUtils,
96 FileUtils,
97 Utils,
98 logger,
99} from '../utils';
3f40bc9c 100
268a74bb 101export class ChargingStation {
c72f6634 102 public readonly index: number;
2484ac1e 103 public readonly templateFile: string;
6e0964c8 104 public stationInfo!: ChargingStationInfo;
452a82ca 105 public started: boolean;
cbf9b878 106 public starting: boolean;
a5e9befc 107 public authorizedTagsCache: AuthorizedTagsCache;
551e477c
JB
108 public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined;
109 public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
72092cfc 110 public wsConnection!: WebSocket | null;
a5e9befc 111 public readonly connectors: Map<number, ConnectorStatus>;
9e23580d 112 public readonly requests: Map<string, CachedRequest>;
551e477c 113 public performanceStatistics!: PerformanceStatistics | undefined;
6e0964c8 114 public heartbeatSetInterval!: NodeJS.Timeout;
6e0964c8 115 public ocppRequestService!: OCPPRequestService;
0a03f36c 116 public bootNotificationRequest!: BootNotificationRequest;
1895299d 117 public bootNotificationResponse!: BootNotificationResponse | undefined;
fa7bccf4 118 public powerDivider!: number;
950b1349 119 private stopping: boolean;
073bd098 120 private configurationFile!: string;
7c72977b 121 private configurationFileHash!: string;
6e0964c8 122 private connectorsConfigurationHash!: string;
a472cf2b 123 private ocppIncomingRequestService!: OCPPIncomingRequestService;
8e242273 124 private readonly messageBuffer: Set<string>;
fa7bccf4 125 private configuredSupervisionUrl!: URL;
c72f6634 126 private configuredSupervisionUrlIndex!: number;
265e4266 127 private wsConnectionRestarted: boolean;
ad2f27c3 128 private autoReconnectRetryCount: number;
72092cfc 129 private templateFileWatcher!: fs.FSWatcher | undefined;
57adbebc 130 private readonly sharedLRUCache: SharedLRUCache;
6e0964c8 131 private webSocketPingSetInterval!: NodeJS.Timeout;
89b7a234 132 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
6af9012e 133
2484ac1e 134 constructor(index: number, templateFile: string) {
aa428a31 135 this.started = false;
950b1349
JB
136 this.starting = false;
137 this.stopping = false;
aa428a31
JB
138 this.wsConnectionRestarted = false;
139 this.autoReconnectRetryCount = 0;
ad2f27c3 140 this.index = index;
2484ac1e 141 this.templateFile = templateFile;
9f2e3130 142 this.connectors = new Map<number, ConnectorStatus>();
32b02249 143 this.requests = new Map<string, CachedRequest>();
8e242273 144 this.messageBuffer = new Set<string>();
b44b779a
JB
145 this.sharedLRUCache = SharedLRUCache.getInstance();
146 this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
89b7a234 147 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
32de5a57 148
9f2e3130 149 this.initialize();
c0560973
JB
150 }
151
25f5a959 152 private get wsConnectionUrl(): URL {
fa7bccf4 153 return new URL(
44eb6026
JB
154 `${
155 this.getSupervisionUrlOcppConfiguration()
156 ? ChargingStationConfigurationUtils.getConfigurationKey(
157 this,
158 this.getSupervisionUrlOcppKey()
72092cfc 159 )?.value
44eb6026
JB
160 : this.configuredSupervisionUrl.href
161 }/${this.stationInfo.chargingStationId}`
fa7bccf4 162 );
12fc74d6
JB
163 }
164
8b7072dc 165 public logPrefix = (): string => {
ccb1d6e9
JB
166 return Utils.logPrefix(
167 ` ${
5a2a53cf 168 (Utils.isNotEmptyString(this?.stationInfo?.chargingStationId) &&
1b271a54
JB
169 this?.stationInfo?.chargingStationId) ??
170 ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile()) ??
171 ''
ccb1d6e9
JB
172 } |`
173 );
8b7072dc 174 };
c0560973 175
c0560973 176 public hasAuthorizedTags(): boolean {
53ac516c 177 return Utils.isNotEmptyArray(
9d7484a4
JB
178 this.authorizedTagsCache.getAuthorizedTags(
179 ChargingStationUtils.getAuthorizationFile(this.stationInfo)
180 )
181 );
c0560973
JB
182 }
183
ad774cec
JB
184 public getEnableStatistics(): boolean {
185 return this.stationInfo.enableStatistics ?? false;
c0560973
JB
186 }
187
ad774cec 188 public getMustAuthorizeAtRemoteStart(): boolean {
03ebf4c1 189 return this.stationInfo.mustAuthorizeAtRemoteStart ?? true;
a7fc8211
JB
190 }
191
ad774cec 192 public getPayloadSchemaValidation(): boolean {
e3018bc4
JB
193 return this.stationInfo.payloadSchemaValidation ?? true;
194 }
195
fa7bccf4
JB
196 public getNumberOfPhases(stationInfo?: ChargingStationInfo): number | undefined {
197 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
198 switch (this.getCurrentOutType(stationInfo)) {
4c2b4904 199 case CurrentType.AC:
fa7bccf4
JB
200 return !Utils.isUndefined(localStationInfo.numberOfPhases)
201 ? localStationInfo.numberOfPhases
e7aeea18 202 : 3;
4c2b4904 203 case CurrentType.DC:
c0560973
JB
204 return 0;
205 }
206 }
207
d5bff457 208 public isWebSocketConnectionOpened(): boolean {
0d8140bd 209 return this?.wsConnection?.readyState === WebSocket.OPEN;
c0560973
JB
210 }
211
1895299d 212 public getRegistrationStatus(): RegistrationStatusEnumType | undefined {
672fed6e
JB
213 return this?.bootNotificationResponse?.status;
214 }
215
73c4266d
JB
216 public isInUnknownState(): boolean {
217 return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
218 }
219
16cd35ad 220 public isInPendingState(): boolean {
d270cc87 221 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING;
16cd35ad
JB
222 }
223
224 public isInAcceptedState(): boolean {
d270cc87 225 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED;
c0560973
JB
226 }
227
16cd35ad 228 public isInRejectedState(): boolean {
d270cc87 229 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED;
16cd35ad
JB
230 }
231
232 public isRegistered(): boolean {
ed6cfcff
JB
233 return (
234 this.isInUnknownState() === false &&
235 (this.isInAcceptedState() === true || this.isInPendingState() === true)
236 );
16cd35ad
JB
237 }
238
c0560973 239 public isChargingStationAvailable(): boolean {
1895299d 240 return this.getConnectorStatus(0)?.availability === AvailabilityType.OPERATIVE;
c0560973
JB
241 }
242
243 public isConnectorAvailable(id: number): boolean {
1895299d 244 return id > 0 && this.getConnectorStatus(id)?.availability === AvailabilityType.OPERATIVE;
c0560973
JB
245 }
246
54544ef1
JB
247 public getNumberOfConnectors(): number {
248 return this.connectors.get(0) ? this.connectors.size - 1 : this.connectors.size;
249 }
250
6d9876e7 251 public getConnectorStatus(id: number): ConnectorStatus | undefined {
734d790d 252 return this.connectors.get(id);
c0560973
JB
253 }
254
fa7bccf4 255 public getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
72092cfc 256 return (stationInfo ?? this.stationInfo)?.currentOutType ?? CurrentType.AC;
c0560973
JB
257 }
258
672fed6e 259 public getOcppStrictCompliance(): boolean {
ccb1d6e9 260 return this.stationInfo?.ocppStrictCompliance ?? false;
672fed6e
JB
261 }
262
fa7bccf4 263 public getVoltageOut(stationInfo?: ChargingStationInfo): number | undefined {
492cf6ab 264 const defaultVoltageOut = ChargingStationUtils.getDefaultVoltageOut(
fa7bccf4 265 this.getCurrentOutType(stationInfo),
492cf6ab
JB
266 this.templateFile,
267 this.logPrefix()
268 );
fa7bccf4
JB
269 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
270 return !Utils.isUndefined(localStationInfo.voltageOut)
271 ? localStationInfo.voltageOut
e7aeea18 272 : defaultVoltageOut;
c0560973
JB
273 }
274
15068be9
JB
275 public getMaximumPower(stationInfo?: ChargingStationInfo): number {
276 const localStationInfo = stationInfo ?? this.stationInfo;
277 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
278 }
279
ad8537a7 280 public getConnectorMaximumAvailablePower(connectorId: number): number {
d20f43b5 281 let connectorAmperageLimitationPowerLimit: number;
b47d68d7
JB
282 if (
283 !Utils.isNullOrUndefined(this.getAmperageLimitation()) &&
72092cfc 284 this.getAmperageLimitation() < this.stationInfo?.maximumAmperage
b47d68d7 285 ) {
4160ae28
JB
286 connectorAmperageLimitationPowerLimit =
287 (this.getCurrentOutType() === CurrentType.AC
cc6e8ab5
JB
288 ? ACElectricUtils.powerTotal(
289 this.getNumberOfPhases(),
290 this.getVoltageOut(),
da57964c 291 this.getAmperageLimitation() * this.getNumberOfConnectors()
cc6e8ab5 292 )
4160ae28 293 : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation())) /
fa7bccf4 294 this.powerDivider;
cc6e8ab5 295 }
fa7bccf4 296 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
15068be9
JB
297 const connectorChargingProfilesPowerLimit =
298 ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
ad8537a7
JB
299 return Math.min(
300 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
301 isNaN(connectorAmperageLimitationPowerLimit)
302 ? Infinity
303 : connectorAmperageLimitationPowerLimit,
15068be9 304 isNaN(connectorChargingProfilesPowerLimit) ? Infinity : connectorChargingProfilesPowerLimit
ad8537a7 305 );
cc6e8ab5
JB
306 }
307
6e0964c8 308 public getTransactionIdTag(transactionId: number): string | undefined {
734d790d 309 for (const connectorId of this.connectors.keys()) {
1895299d
JB
310 if (
311 connectorId > 0 &&
312 this.getConnectorStatus(connectorId)?.transactionId === transactionId
313 ) {
72092cfc 314 return this.getConnectorStatus(connectorId)?.transactionIdTag;
c0560973
JB
315 }
316 }
317 }
318
6ed92bc1 319 public getOutOfOrderEndMeterValues(): boolean {
ccb1d6e9 320 return this.stationInfo?.outOfOrderEndMeterValues ?? false;
6ed92bc1
JB
321 }
322
323 public getBeginEndMeterValues(): boolean {
ccb1d6e9 324 return this.stationInfo?.beginEndMeterValues ?? false;
6ed92bc1
JB
325 }
326
327 public getMeteringPerTransaction(): boolean {
ccb1d6e9 328 return this.stationInfo?.meteringPerTransaction ?? true;
6ed92bc1
JB
329 }
330
fd0c36fa 331 public getTransactionDataMeterValues(): boolean {
ccb1d6e9 332 return this.stationInfo?.transactionDataMeterValues ?? false;
fd0c36fa
JB
333 }
334
9ccca265 335 public getMainVoltageMeterValues(): boolean {
ccb1d6e9 336 return this.stationInfo?.mainVoltageMeterValues ?? true;
9ccca265
JB
337 }
338
6b10669b 339 public getPhaseLineToLineVoltageMeterValues(): boolean {
ccb1d6e9 340 return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
9bd87386
JB
341 }
342
7bc31f9c 343 public getCustomValueLimitationMeterValues(): boolean {
ccb1d6e9 344 return this.stationInfo?.customValueLimitationMeterValues ?? true;
7bc31f9c
JB
345 }
346
f479a792 347 public getConnectorIdByTransactionId(transactionId: number): number | undefined {
734d790d 348 for (const connectorId of this.connectors.keys()) {
f479a792
JB
349 if (
350 connectorId > 0 &&
351 this.getConnectorStatus(connectorId)?.transactionId === transactionId
352 ) {
353 return connectorId;
c0560973
JB
354 }
355 }
356 }
357
07989fad
JB
358 public getEnergyActiveImportRegisterByTransactionId(
359 transactionId: number,
18bf8274 360 rounded = false
07989fad
JB
361 ): number {
362 return this.getEnergyActiveImportRegister(
363 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
18bf8274 364 rounded
cbad1217 365 );
cbad1217
JB
366 }
367
18bf8274
JB
368 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
369 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded);
6ed92bc1
JB
370 }
371
c0560973 372 public getAuthorizeRemoteTxRequests(): boolean {
17ac262c
JB
373 const authorizeRemoteTxRequests = ChargingStationConfigurationUtils.getConfigurationKey(
374 this,
e7aeea18
JB
375 StandardParametersKey.AuthorizeRemoteTxRequests
376 );
377 return authorizeRemoteTxRequests
378 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
379 : false;
c0560973
JB
380 }
381
382 public getLocalAuthListEnabled(): boolean {
17ac262c
JB
383 const localAuthListEnabled = ChargingStationConfigurationUtils.getConfigurationKey(
384 this,
e7aeea18
JB
385 StandardParametersKey.LocalAuthListEnabled
386 );
c0560973
JB
387 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
388 }
389
c0560973 390 public startHeartbeat(): void {
e7aeea18
JB
391 if (
392 this.getHeartbeatInterval() &&
393 this.getHeartbeatInterval() > 0 &&
394 !this.heartbeatSetInterval
395 ) {
6a8329b4
JB
396 this.heartbeatSetInterval = setInterval(() => {
397 this.ocppRequestService
398 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
72092cfc 399 .catch((error) => {
6a8329b4
JB
400 logger.error(
401 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
402 error
403 );
404 });
c0560973 405 }, this.getHeartbeatInterval());
e7aeea18 406 logger.info(
44eb6026
JB
407 `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
408 this.getHeartbeatInterval()
409 )}`
e7aeea18 410 );
c0560973 411 } else if (this.heartbeatSetInterval) {
e7aeea18 412 logger.info(
44eb6026
JB
413 `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
414 this.getHeartbeatInterval()
415 )}`
e7aeea18 416 );
c0560973 417 } else {
e7aeea18
JB
418 logger.error(
419 `${this.logPrefix()} Heartbeat interval set to ${
420 this.getHeartbeatInterval()
421 ? Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
422 : this.getHeartbeatInterval()
423 }, not starting the heartbeat`
424 );
c0560973
JB
425 }
426 }
427
428 public restartHeartbeat(): void {
429 // Stop heartbeat
430 this.stopHeartbeat();
431 // Start heartbeat
432 this.startHeartbeat();
433 }
434
17ac262c
JB
435 public restartWebSocketPing(): void {
436 // Stop WebSocket ping
437 this.stopWebSocketPing();
438 // Start WebSocket ping
439 this.startWebSocketPing();
440 }
441
c0560973
JB
442 public startMeterValues(connectorId: number, interval: number): void {
443 if (connectorId === 0) {
e7aeea18
JB
444 logger.error(
445 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
446 );
c0560973
JB
447 return;
448 }
734d790d 449 if (!this.getConnectorStatus(connectorId)) {
e7aeea18
JB
450 logger.error(
451 `${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`
452 );
c0560973
JB
453 return;
454 }
5e3cb728 455 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
e7aeea18
JB
456 logger.error(
457 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
458 );
c0560973 459 return;
e7aeea18 460 } else if (
5e3cb728 461 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
d812bdcb 462 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
e7aeea18
JB
463 ) {
464 logger.error(
465 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`
466 );
c0560973
JB
467 return;
468 }
469 if (interval > 0) {
6a8329b4
JB
470 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(() => {
471 // FIXME: Implement OCPP version agnostic helpers
472 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
473 this,
474 connectorId,
475 this.getConnectorStatus(connectorId).transactionId,
476 interval
477 );
478 this.ocppRequestService
479 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
08f130a0 480 this,
f22266fd
JB
481 RequestCommand.METER_VALUES,
482 {
483 connectorId,
72092cfc 484 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
f22266fd
JB
485 meterValue: [meterValue],
486 }
6a8329b4 487 )
72092cfc 488 .catch((error) => {
6a8329b4
JB
489 logger.error(
490 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
491 error
492 );
493 });
494 }, interval);
c0560973 495 } else {
e7aeea18
JB
496 logger.error(
497 `${this.logPrefix()} Charging station ${
498 StandardParametersKey.MeterValueSampleInterval
499 } configuration set to ${
500 interval ? Utils.formatDurationMilliSeconds(interval) : interval
501 }, not sending MeterValues`
502 );
c0560973
JB
503 }
504 }
505
506 public start(): void {
0d8852a5
JB
507 if (this.started === false) {
508 if (this.starting === false) {
509 this.starting = true;
ad774cec 510 if (this.getEnableStatistics() === true) {
551e477c 511 this.performanceStatistics?.start();
0d8852a5
JB
512 }
513 this.openWSConnection();
514 // Monitor charging station template file
515 this.templateFileWatcher = FileUtils.watchJsonFile(
0d8852a5 516 this.templateFile,
7164966d
JB
517 FileType.ChargingStationTemplate,
518 this.logPrefix(),
519 undefined,
0d8852a5 520 (event, filename): void => {
5a2a53cf 521 if (Utils.isNotEmptyString(filename) && event === 'change') {
0d8852a5
JB
522 try {
523 logger.debug(
524 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
525 this.templateFile
526 } file have changed, reload`
527 );
528 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
529 // Initialize
530 this.initialize();
531 // Restart the ATG
532 this.stopAutomaticTransactionGenerator();
533 if (
534 this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true
535 ) {
536 this.startAutomaticTransactionGenerator();
537 }
ad774cec 538 if (this.getEnableStatistics() === true) {
551e477c 539 this.performanceStatistics?.restart();
0d8852a5 540 } else {
551e477c 541 this.performanceStatistics?.stop();
0d8852a5
JB
542 }
543 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
544 } catch (error) {
545 logger.error(
546 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
547 error
548 );
950b1349 549 }
a95873d8 550 }
a95873d8 551 }
0d8852a5 552 );
56eb297e 553 this.started = true;
1895299d 554 parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
0d8852a5
JB
555 this.starting = false;
556 } else {
557 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
558 }
950b1349 559 } else {
0d8852a5 560 logger.warn(`${this.logPrefix()} Charging station is already started...`);
950b1349 561 }
c0560973
JB
562 }
563
60ddad53 564 public async stop(reason?: StopTransactionReason): Promise<void> {
0d8852a5
JB
565 if (this.started === true) {
566 if (this.stopping === false) {
567 this.stopping = true;
568 await this.stopMessageSequence(reason);
0d8852a5 569 this.closeWSConnection();
ad774cec 570 if (this.getEnableStatistics() === true) {
551e477c 571 this.performanceStatistics?.stop();
0d8852a5
JB
572 }
573 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
72092cfc 574 this.templateFileWatcher?.close();
0d8852a5 575 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
cdde2cfe 576 delete this.bootNotificationResponse;
0d8852a5 577 this.started = false;
1895299d 578 parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
0d8852a5
JB
579 this.stopping = false;
580 } else {
581 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
c0560973 582 }
950b1349 583 } else {
0d8852a5 584 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
c0560973 585 }
c0560973
JB
586 }
587
60ddad53
JB
588 public async reset(reason?: StopTransactionReason): Promise<void> {
589 await this.stop(reason);
94ec7e96 590 await Utils.sleep(this.stationInfo.resetTime);
fa7bccf4 591 this.initialize();
94ec7e96
JB
592 this.start();
593 }
594
17ac262c
JB
595 public saveOcppConfiguration(): void {
596 if (this.getOcppPersistentConfiguration()) {
7c72977b 597 this.saveConfiguration();
e6895390
JB
598 }
599 }
600
a2653482
JB
601 public resetConnectorStatus(connectorId: number): void {
602 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
603 this.getConnectorStatus(connectorId).idTagAuthorized = false;
604 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d 605 this.getConnectorStatus(connectorId).transactionStarted = false;
d812bdcb
JB
606 delete this.getConnectorStatus(connectorId)?.localAuthorizeIdTag;
607 delete this.getConnectorStatus(connectorId)?.authorizeIdTag;
608 delete this.getConnectorStatus(connectorId)?.transactionId;
609 delete this.getConnectorStatus(connectorId)?.transactionIdTag;
734d790d 610 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
d812bdcb 611 delete this.getConnectorStatus(connectorId)?.transactionBeginMeterValue;
dd119a6b 612 this.stopMeterValues(connectorId);
1895299d 613 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
614 }
615
72092cfc 616 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
17ac262c
JB
617 return ChargingStationConfigurationUtils.getConfigurationKey(
618 this,
619 StandardParametersKey.SupportedFeatureProfiles
72092cfc 620 )?.value?.includes(featureProfile);
68cb8b91
JB
621 }
622
8e242273
JB
623 public bufferMessage(message: string): void {
624 this.messageBuffer.add(message);
3ba2381e
JB
625 }
626
db2336d9 627 public openWSConnection(
59b6ed8d 628 options: WsOptions = this.stationInfo?.wsOptions ?? Constants.EMPTY_OBJECT,
db2336d9
JB
629 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
630 closeOpened: false,
631 terminateOpened: false,
632 }
633 ): void {
634 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
635 params.closeOpened = params?.closeOpened ?? false;
636 params.terminateOpened = params?.terminateOpened ?? false;
cbf9b878 637 if (this.started === false && this.starting === false) {
d1c6c833
JB
638 logger.warn(
639 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
640 );
641 return;
642 }
db2336d9
JB
643 if (
644 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
645 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
646 ) {
647 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
648 }
649 if (params?.closeOpened) {
650 this.closeWSConnection();
651 }
652 if (params?.terminateOpened) {
653 this.terminateWSConnection();
654 }
db2336d9 655
56eb297e 656 if (this.isWebSocketConnectionOpened() === true) {
0a03f36c
JB
657 logger.warn(
658 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
659 );
660 return;
661 }
662
db2336d9 663 logger.info(
0a03f36c 664 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
db2336d9
JB
665 );
666
feff11ec
JB
667 this.wsConnection = new WebSocket(
668 this.wsConnectionUrl,
669 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
670 options
671 );
db2336d9
JB
672
673 // Handle WebSocket message
674 this.wsConnection.on(
675 'message',
676 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
677 );
678 // Handle WebSocket error
679 this.wsConnection.on(
680 'error',
681 this.onError.bind(this) as (this: WebSocket, error: Error) => void
682 );
683 // Handle WebSocket close
684 this.wsConnection.on(
685 'close',
686 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
687 );
688 // Handle WebSocket open
689 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
690 // Handle WebSocket ping
691 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
692 // Handle WebSocket pong
693 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
694 }
695
696 public closeWSConnection(): void {
56eb297e 697 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 698 this.wsConnection?.close();
db2336d9
JB
699 this.wsConnection = null;
700 }
701 }
702
8f879946
JB
703 public startAutomaticTransactionGenerator(
704 connectorIds?: number[],
705 automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
706 ): void {
707 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
708 automaticTransactionGeneratorConfiguration ??
4f69be04 709 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
8f879946
JB
710 this
711 );
53ac516c 712 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc 713 for (const connectorId of connectorIds) {
551e477c 714 this.automaticTransactionGenerator?.startConnector(connectorId);
a5e9befc
JB
715 }
716 } else {
551e477c 717 this.automaticTransactionGenerator?.start();
4f69be04 718 }
1895299d 719 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
720 }
721
a5e9befc 722 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
53ac516c 723 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc
JB
724 for (const connectorId of connectorIds) {
725 this.automaticTransactionGenerator?.stopConnector(connectorId);
726 }
727 } else {
728 this.automaticTransactionGenerator?.stop();
4f69be04 729 }
1895299d 730 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
731 }
732
5e3cb728
JB
733 public async stopTransactionOnConnector(
734 connectorId: number,
735 reason = StopTransactionReason.NONE
736 ): Promise<StopTransactionResponse> {
72092cfc 737 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
5e3cb728 738 if (
c7e8e0a2
JB
739 this.getBeginEndMeterValues() === true &&
740 this.getOcppStrictCompliance() === true &&
741 this.getOutOfOrderEndMeterValues() === false
5e3cb728
JB
742 ) {
743 // FIXME: Implement OCPP version agnostic helpers
744 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
745 this,
746 connectorId,
747 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
748 );
749 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
750 this,
751 RequestCommand.METER_VALUES,
752 {
753 connectorId,
754 transactionId,
755 meterValue: [transactionEndMeterValue],
756 }
757 );
758 }
759 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
760 this,
761 RequestCommand.STOP_TRANSACTION,
762 {
763 transactionId,
764 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
5e3cb728
JB
765 reason,
766 }
767 );
768 }
769
f90c1757 770 private flushMessageBuffer(): void {
8e242273 771 if (this.messageBuffer.size > 0) {
72092cfc 772 this.messageBuffer.forEach((message) => {
1431af78
JB
773 let beginId: string;
774 let commandName: RequestCommand;
8ca6874c 775 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
776 const isRequest = messageType === MessageType.CALL_MESSAGE;
777 if (isRequest) {
778 [, , commandName] = JSON.parse(message) as OutgoingRequest;
779 beginId = PerformanceStatistics.beginMeasure(commandName);
780 }
72092cfc 781 this.wsConnection?.send(message);
1431af78 782 isRequest && PerformanceStatistics.endMeasure(commandName, beginId);
8ca6874c
JB
783 logger.debug(
784 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
785 messageType
786 )} payload sent: ${message}`
787 );
8e242273 788 this.messageBuffer.delete(message);
77f00f84
JB
789 });
790 }
791 }
792
1f5df42a
JB
793 private getSupervisionUrlOcppConfiguration(): boolean {
794 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
795 }
796
e8e865ea 797 private getSupervisionUrlOcppKey(): string {
6dad8e21 798 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
e8e865ea
JB
799 }
800
72092cfc
JB
801 private getTemplateFromFile(): ChargingStationTemplate | undefined {
802 let template: ChargingStationTemplate;
5ad8570f 803 try {
57adbebc
JB
804 if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) {
805 template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash);
7c72977b
JB
806 } else {
807 const measureId = `${FileType.ChargingStationTemplate} read`;
808 const beginId = PerformanceStatistics.beginMeasure(measureId);
809 template = JSON.parse(
810 fs.readFileSync(this.templateFile, 'utf8')
811 ) as ChargingStationTemplate;
812 PerformanceStatistics.endMeasure(measureId, beginId);
813 template.templateHash = crypto
814 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
815 .update(JSON.stringify(template))
816 .digest('hex');
57adbebc 817 this.sharedLRUCache.setChargingStationTemplate(template);
7c72977b 818 }
5ad8570f 819 } catch (error) {
e7aeea18 820 FileUtils.handleFileException(
2484ac1e 821 this.templateFile,
7164966d
JB
822 FileType.ChargingStationTemplate,
823 error as NodeJS.ErrnoException,
824 this.logPrefix()
e7aeea18 825 );
5ad8570f 826 }
2484ac1e
JB
827 return template;
828 }
829
7a3a2ebb 830 private getStationInfoFromTemplate(): ChargingStationInfo {
72092cfc 831 const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
fa7bccf4 832 if (Utils.isNullOrUndefined(stationTemplate)) {
eaad6e5c 833 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
ccb1d6e9
JB
834 logger.error(`${this.logPrefix()} ${errorMsg}`);
835 throw new BaseError(errorMsg);
94ec7e96 836 }
fa7bccf4 837 if (Utils.isEmptyObject(stationTemplate)) {
ccb1d6e9
JB
838 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
839 logger.error(`${this.logPrefix()} ${errorMsg}`);
840 throw new BaseError(errorMsg);
94ec7e96 841 }
2dcfe98e 842 // Deprecation template keys section
17ac262c 843 ChargingStationUtils.warnDeprecatedTemplateKey(
fa7bccf4 844 stationTemplate,
e7aeea18 845 'supervisionUrl',
17ac262c 846 this.templateFile,
ccb1d6e9 847 this.logPrefix(),
e7aeea18
JB
848 "Use 'supervisionUrls' instead"
849 );
17ac262c 850 ChargingStationUtils.convertDeprecatedTemplateKey(
fa7bccf4 851 stationTemplate,
17ac262c
JB
852 'supervisionUrl',
853 'supervisionUrls'
854 );
fa7bccf4
JB
855 const stationInfo: ChargingStationInfo =
856 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
51c83d6f 857 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
fa7bccf4
JB
858 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
859 this.index,
860 stationTemplate
861 );
72092cfc 862 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fa7bccf4 863 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
53ac516c 864 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
551e477c 865 stationTemplate.power = stationTemplate.power as number[];
fa7bccf4 866 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
cc6e8ab5 867 stationInfo.maximumPower =
72092cfc 868 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
869 ? stationTemplate.power[powerArrayRandomIndex] * 1000
870 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 871 } else {
551e477c 872 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 873 stationInfo.maximumPower =
72092cfc 874 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
875 ? stationTemplate.power * 1000
876 : stationTemplate.power;
877 }
3637ca2c 878 stationInfo.firmwareVersionPattern =
72092cfc 879 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 880 if (
5a2a53cf 881 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
3637ca2c
JB
882 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
883 ) {
884 logger.warn(
885 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
886 this.templateFile
887 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
888 );
889 }
598c886d 890 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 891 {
598c886d
JB
892 versionUpgrade: {
893 step: 1,
894 },
15748260
JB
895 reset: true,
896 },
59b6ed8d 897 stationTemplate?.firmwareUpgrade ?? Constants.EMPTY_OBJECT
15748260 898 );
d812bdcb 899 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
fa7bccf4 900 ? stationTemplate.resetTime * 1000
e7aeea18 901 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
c72f6634
JB
902 const configuredMaxConnectors =
903 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
fa7bccf4
JB
904 ChargingStationUtils.checkConfiguredMaxConnectors(
905 configuredMaxConnectors,
906 this.templateFile,
fc040c43 907 this.logPrefix()
fa7bccf4
JB
908 );
909 const templateMaxConnectors =
910 ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
911 ChargingStationUtils.checkTemplateMaxConnectors(
912 templateMaxConnectors,
913 this.templateFile,
fc040c43 914 this.logPrefix()
fa7bccf4
JB
915 );
916 if (
917 configuredMaxConnectors >
918 (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
919 !stationTemplate?.randomConnectors
920 ) {
921 logger.warn(
922 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
923 this.templateFile
924 }, forcing random connector configurations affectation`
925 );
926 stationInfo.randomConnectors = true;
927 }
928 // Build connectors if needed (FIXME: should be factored out)
929 this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors);
930 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
931 ChargingStationUtils.createStationInfoHash(stationInfo);
9ac86a7e 932 return stationInfo;
5ad8570f
JB
933 }
934
551e477c
JB
935 private getStationInfoFromFile(): ChargingStationInfo | undefined {
936 let stationInfo: ChargingStationInfo | undefined;
fa7bccf4 937 this.getStationInfoPersistentConfiguration() &&
551e477c 938 (stationInfo = this.getConfigurationFromFile()?.stationInfo);
fa7bccf4 939 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
f765beaa 940 return stationInfo;
2484ac1e
JB
941 }
942
943 private getStationInfo(): ChargingStationInfo {
944 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 945 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
aca53a1a 946 // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
f765beaa 947 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
01efc60a
JB
948 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
949 return this.stationInfo;
950 }
2484ac1e 951 return stationInfoFromFile;
f765beaa 952 }
fec4d204
JB
953 stationInfoFromFile &&
954 ChargingStationUtils.propagateSerialNumber(
955 this.getTemplateFromFile(),
956 stationInfoFromFile,
957 stationInfoFromTemplate
958 );
01efc60a 959 return stationInfoFromTemplate;
2484ac1e
JB
960 }
961
962 private saveStationInfo(): void {
ccb1d6e9 963 if (this.getStationInfoPersistentConfiguration()) {
7c72977b 964 this.saveConfiguration();
ccb1d6e9 965 }
2484ac1e
JB
966 }
967
e8e865ea 968 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
969 return this.stationInfo?.ocppPersistentConfiguration ?? true;
970 }
971
972 private getStationInfoPersistentConfiguration(): boolean {
973 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
974 }
975
c0560973 976 private handleUnsupportedVersion(version: OCPPVersion) {
fc040c43
JB
977 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
978 logger.error(`${this.logPrefix()} ${errMsg}`);
6c8f5d90 979 throw new BaseError(errMsg);
c0560973
JB
980 }
981
2484ac1e 982 private initialize(): void {
fa7bccf4 983 this.configurationFile = path.join(
ee5f26a2 984 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
44eb6026 985 `${ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile())}.json`
0642c3d2 986 );
b44b779a
JB
987 this.stationInfo = this.getStationInfo();
988 this.saveStationInfo();
7a3a2ebb 989 // Avoid duplication of connectors related information in RAM
cdde2cfe 990 delete this.stationInfo?.Connectors;
fa7bccf4 991 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
ad774cec 992 if (this.getEnableStatistics() === true) {
0642c3d2 993 this.performanceStatistics = PerformanceStatistics.getInstance(
51c83d6f 994 this.stationInfo.hashId,
0642c3d2 995 this.stationInfo.chargingStationId,
fa7bccf4 996 this.configuredSupervisionUrl
0642c3d2
JB
997 );
998 }
fa7bccf4
JB
999 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1000 this.stationInfo
1001 );
fa7bccf4
JB
1002 this.powerDivider = this.getPowerDivider();
1003 // OCPP configuration
1004 this.ocppConfiguration = this.getOcppConfiguration();
1005 this.initializeOcppConfiguration();
feff11ec 1006 this.initializeOcppServices();
b7f9e41d 1007 if (this.stationInfo?.autoRegister === true) {
47e22477 1008 this.bootNotificationResponse = {
d270cc87 1009 currentTime: new Date(),
47e22477 1010 interval: this.getHeartbeatInterval() / 1000,
d270cc87 1011 status: RegistrationStatusEnumType.ACCEPTED,
47e22477
JB
1012 };
1013 }
3637ca2c
JB
1014 if (
1015 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
5a2a53cf
JB
1016 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1017 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1018 ) {
d812bdcb 1019 const patternGroup: number | undefined =
15748260 1020 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1021 this.stationInfo.firmwareVersion?.split('.').length;
72092cfc
JB
1022 const match = this.stationInfo?.firmwareVersion
1023 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1024 ?.slice(1, patternGroup + 1);
3637ca2c 1025 const patchLevelIndex = match.length - 1;
5d280aae 1026 match[patchLevelIndex] = (
07c52a72
JB
1027 Utils.convertToInt(match[patchLevelIndex]) +
1028 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
5d280aae 1029 ).toString();
72092cfc 1030 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1031 }
147d0e0f
JB
1032 }
1033
feff11ec
JB
1034 private initializeOcppServices(): void {
1035 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1036 switch (ocppVersion) {
1037 case OCPPVersion.VERSION_16:
1038 this.ocppIncomingRequestService =
1039 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1040 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1041 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1042 );
1043 break;
1044 case OCPPVersion.VERSION_20:
1045 case OCPPVersion.VERSION_201:
1046 this.ocppIncomingRequestService =
1047 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1048 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1049 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1050 );
1051 break;
1052 default:
1053 this.handleUnsupportedVersion(ocppVersion);
1054 break;
1055 }
1056 }
1057
2484ac1e 1058 private initializeOcppConfiguration(): void {
17ac262c
JB
1059 if (
1060 !ChargingStationConfigurationUtils.getConfigurationKey(
1061 this,
1062 StandardParametersKey.HeartbeatInterval
1063 )
1064 ) {
1065 ChargingStationConfigurationUtils.addConfigurationKey(
1066 this,
1067 StandardParametersKey.HeartbeatInterval,
1068 '0'
1069 );
f0f65a62 1070 }
17ac262c
JB
1071 if (
1072 !ChargingStationConfigurationUtils.getConfigurationKey(
1073 this,
1074 StandardParametersKey.HeartBeatInterval
1075 )
1076 ) {
1077 ChargingStationConfigurationUtils.addConfigurationKey(
1078 this,
1079 StandardParametersKey.HeartBeatInterval,
1080 '0',
1081 { visible: false }
1082 );
f0f65a62 1083 }
e7aeea18
JB
1084 if (
1085 this.getSupervisionUrlOcppConfiguration() &&
17ac262c 1086 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1087 ) {
17ac262c
JB
1088 ChargingStationConfigurationUtils.addConfigurationKey(
1089 this,
a59737e3 1090 this.getSupervisionUrlOcppKey(),
fa7bccf4 1091 this.configuredSupervisionUrl.href,
e7aeea18
JB
1092 { reboot: true }
1093 );
e6895390
JB
1094 } else if (
1095 !this.getSupervisionUrlOcppConfiguration() &&
17ac262c 1096 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1097 ) {
17ac262c
JB
1098 ChargingStationConfigurationUtils.deleteConfigurationKey(
1099 this,
1100 this.getSupervisionUrlOcppKey(),
1101 { save: false }
1102 );
12fc74d6 1103 }
cc6e8ab5 1104 if (
5a2a53cf 1105 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1106 !ChargingStationConfigurationUtils.getConfigurationKey(
1107 this,
1108 this.stationInfo.amperageLimitationOcppKey
1109 )
cc6e8ab5 1110 ) {
17ac262c
JB
1111 ChargingStationConfigurationUtils.addConfigurationKey(
1112 this,
cc6e8ab5 1113 this.stationInfo.amperageLimitationOcppKey,
17ac262c
JB
1114 (
1115 this.stationInfo.maximumAmperage *
1116 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1117 ).toString()
cc6e8ab5
JB
1118 );
1119 }
17ac262c
JB
1120 if (
1121 !ChargingStationConfigurationUtils.getConfigurationKey(
1122 this,
1123 StandardParametersKey.SupportedFeatureProfiles
1124 )
1125 ) {
1126 ChargingStationConfigurationUtils.addConfigurationKey(
1127 this,
e7aeea18 1128 StandardParametersKey.SupportedFeatureProfiles,
b22787b4 1129 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
e7aeea18
JB
1130 );
1131 }
17ac262c
JB
1132 ChargingStationConfigurationUtils.addConfigurationKey(
1133 this,
e7aeea18
JB
1134 StandardParametersKey.NumberOfConnectors,
1135 this.getNumberOfConnectors().toString(),
a95873d8
JB
1136 { readonly: true },
1137 { overwrite: true }
e7aeea18 1138 );
17ac262c
JB
1139 if (
1140 !ChargingStationConfigurationUtils.getConfigurationKey(
1141 this,
1142 StandardParametersKey.MeterValuesSampledData
1143 )
1144 ) {
1145 ChargingStationConfigurationUtils.addConfigurationKey(
1146 this,
e7aeea18
JB
1147 StandardParametersKey.MeterValuesSampledData,
1148 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1149 );
7abfea5f 1150 }
17ac262c
JB
1151 if (
1152 !ChargingStationConfigurationUtils.getConfigurationKey(
1153 this,
1154 StandardParametersKey.ConnectorPhaseRotation
1155 )
1156 ) {
7e1dc878 1157 const connectorPhaseRotation = [];
734d790d 1158 for (const connectorId of this.connectors.keys()) {
7e1dc878 1159 // AC/DC
734d790d
JB
1160 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1161 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1162 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1163 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
e7aeea18 1164 // AC
734d790d
JB
1165 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1166 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1167 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1168 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
7e1dc878
JB
1169 }
1170 }
17ac262c
JB
1171 ChargingStationConfigurationUtils.addConfigurationKey(
1172 this,
e7aeea18
JB
1173 StandardParametersKey.ConnectorPhaseRotation,
1174 connectorPhaseRotation.toString()
1175 );
7e1dc878 1176 }
e7aeea18 1177 if (
17ac262c
JB
1178 !ChargingStationConfigurationUtils.getConfigurationKey(
1179 this,
1180 StandardParametersKey.AuthorizeRemoteTxRequests
e7aeea18
JB
1181 )
1182 ) {
17ac262c
JB
1183 ChargingStationConfigurationUtils.addConfigurationKey(
1184 this,
1185 StandardParametersKey.AuthorizeRemoteTxRequests,
1186 'true'
1187 );
36f6a92e 1188 }
17ac262c
JB
1189 if (
1190 !ChargingStationConfigurationUtils.getConfigurationKey(
1191 this,
1192 StandardParametersKey.LocalAuthListEnabled
1193 ) &&
1194 ChargingStationConfigurationUtils.getConfigurationKey(
1195 this,
1196 StandardParametersKey.SupportedFeatureProfiles
72092cfc 1197 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
17ac262c
JB
1198 ) {
1199 ChargingStationConfigurationUtils.addConfigurationKey(
1200 this,
1201 StandardParametersKey.LocalAuthListEnabled,
1202 'false'
1203 );
1204 }
1205 if (
1206 !ChargingStationConfigurationUtils.getConfigurationKey(
1207 this,
1208 StandardParametersKey.ConnectionTimeOut
1209 )
1210 ) {
1211 ChargingStationConfigurationUtils.addConfigurationKey(
1212 this,
e7aeea18
JB
1213 StandardParametersKey.ConnectionTimeOut,
1214 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1215 );
8bce55bf 1216 }
2484ac1e 1217 this.saveOcppConfiguration();
073bd098
JB
1218 }
1219
3d25cc86
JB
1220 private initializeConnectors(
1221 stationInfo: ChargingStationInfo,
fa7bccf4 1222 configuredMaxConnectors: number,
3d25cc86
JB
1223 templateMaxConnectors: number
1224 ): void {
1225 if (!stationInfo?.Connectors && this.connectors.size === 0) {
fc040c43
JB
1226 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1227 logger.error(`${this.logPrefix()} ${logMsg}`);
3d25cc86
JB
1228 throw new BaseError(logMsg);
1229 }
1230 if (!stationInfo?.Connectors[0]) {
1231 logger.warn(
1232 `${this.logPrefix()} Charging station information from template ${
1233 this.templateFile
1234 } with no connector Id 0 configuration`
1235 );
1236 }
1237 if (stationInfo?.Connectors) {
1238 const connectorsConfigHash = crypto
1239 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
14ecae6a 1240 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
3d25cc86
JB
1241 .digest('hex');
1242 const connectorsConfigChanged =
1243 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1244 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1245 connectorsConfigChanged && this.connectors.clear();
1246 this.connectorsConfigurationHash = connectorsConfigHash;
1247 // Add connector Id 0
1248 let lastConnector = '0';
1249 for (lastConnector in stationInfo?.Connectors) {
56eb297e 1250 const connectorStatus = stationInfo?.Connectors[lastConnector];
3d25cc86
JB
1251 const lastConnectorId = Utils.convertToInt(lastConnector);
1252 if (
1253 lastConnectorId === 0 &&
bb83b5ed 1254 this.getUseConnectorId0(stationInfo) === true &&
56eb297e 1255 connectorStatus
3d25cc86 1256 ) {
56eb297e 1257 this.checkStationInfoConnectorStatus(lastConnectorId, connectorStatus);
3d25cc86
JB
1258 this.connectors.set(
1259 lastConnectorId,
56eb297e 1260 Utils.cloneObject<ConnectorStatus>(connectorStatus)
3d25cc86
JB
1261 );
1262 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1263 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1264 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1265 }
1266 }
1267 }
1268 // Generate all connectors
1269 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
fa7bccf4 1270 for (let index = 1; index <= configuredMaxConnectors; index++) {
ccb1d6e9 1271 const randConnectorId = stationInfo?.randomConnectors
3d25cc86
JB
1272 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1273 : index;
56eb297e
JB
1274 const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
1275 this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
1276 this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86
JB
1277 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1278 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1279 this.getConnectorStatus(index).chargingProfiles = [];
1280 }
1281 }
1282 }
1283 }
1284 } else {
1285 logger.warn(
1286 `${this.logPrefix()} Charging station information from template ${
1287 this.templateFile
1288 } with no connectors configuration defined, using already defined connectors`
1289 );
1290 }
1291 // Initialize transaction attributes on connectors
1292 for (const connectorId of this.connectors.keys()) {
72092cfc 1293 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
f5b51071
JB
1294 logger.warn(
1295 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
72092cfc 1296 this.getConnectorStatus(connectorId)?.transactionId
f5b51071
JB
1297 }`
1298 );
1299 }
1984f194
JB
1300 if (
1301 connectorId > 0 &&
72092cfc
JB
1302 (this.getConnectorStatus(connectorId)?.transactionStarted === undefined ||
1303 this.getConnectorStatus(connectorId)?.transactionStarted === null)
1984f194 1304 ) {
3d25cc86
JB
1305 this.initializeConnectorStatus(connectorId);
1306 }
1307 }
1308 }
1309
56eb297e
JB
1310 private checkStationInfoConnectorStatus(
1311 connectorId: number,
1312 connectorStatus: ConnectorStatus
1313 ): void {
1314 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1315 logger.warn(
1316 `${this.logPrefix()} Charging station information from template ${
1317 this.templateFile
1318 } with connector ${connectorId} status configuration defined, undefine it`
1319 );
cdde2cfe 1320 delete connectorStatus.status;
56eb297e
JB
1321 }
1322 }
1323
551e477c
JB
1324 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1325 let configuration: ChargingStationConfiguration | undefined;
2484ac1e 1326 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1327 try {
57adbebc
JB
1328 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1329 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1330 this.configurationFileHash
1331 );
7c72977b
JB
1332 } else {
1333 const measureId = `${FileType.ChargingStationConfiguration} read`;
1334 const beginId = PerformanceStatistics.beginMeasure(measureId);
1335 configuration = JSON.parse(
1336 fs.readFileSync(this.configurationFile, 'utf8')
1337 ) as ChargingStationConfiguration;
1338 PerformanceStatistics.endMeasure(measureId, beginId);
1339 this.configurationFileHash = configuration.configurationHash;
57adbebc 1340 this.sharedLRUCache.setChargingStationConfiguration(configuration);
7c72977b 1341 }
073bd098
JB
1342 } catch (error) {
1343 FileUtils.handleFileException(
073bd098 1344 this.configurationFile,
7164966d
JB
1345 FileType.ChargingStationConfiguration,
1346 error as NodeJS.ErrnoException,
1347 this.logPrefix()
073bd098
JB
1348 );
1349 }
1350 }
1351 return configuration;
1352 }
1353
7c72977b 1354 private saveConfiguration(): void {
2484ac1e
JB
1355 if (this.configurationFile) {
1356 try {
2484ac1e
JB
1357 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1358 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1359 }
ccb1d6e9 1360 const configurationData: ChargingStationConfiguration =
59b6ed8d 1361 Utils.cloneObject(this.getConfigurationFromFile()) ?? Constants.EMPTY_OBJECT;
7c72977b
JB
1362 this.ocppConfiguration?.configurationKey &&
1363 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1364 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1365 delete configurationData.configurationHash;
1366 const configurationHash = crypto
1367 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1368 .update(JSON.stringify(configurationData))
1369 .digest('hex');
1370 if (this.configurationFileHash !== configurationHash) {
1371 configurationData.configurationHash = configurationHash;
1372 const measureId = `${FileType.ChargingStationConfiguration} write`;
1373 const beginId = PerformanceStatistics.beginMeasure(measureId);
1374 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1375 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1376 fs.closeSync(fileDescriptor);
1377 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1378 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1379 this.configurationFileHash = configurationHash;
57adbebc 1380 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1381 } else {
1382 logger.debug(
1383 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1384 this.configurationFile
1385 }`
1386 );
2484ac1e 1387 }
2484ac1e
JB
1388 } catch (error) {
1389 FileUtils.handleFileException(
2484ac1e 1390 this.configurationFile,
7164966d
JB
1391 FileType.ChargingStationConfiguration,
1392 error as NodeJS.ErrnoException,
1393 this.logPrefix()
073bd098
JB
1394 );
1395 }
2484ac1e
JB
1396 } else {
1397 logger.error(
01efc60a 1398 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1399 );
073bd098
JB
1400 }
1401 }
1402
551e477c
JB
1403 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1404 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1405 }
1406
551e477c
JB
1407 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1408 let configuration: ChargingStationConfiguration | undefined;
23290150 1409 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1410 const configurationFromFile = this.getConfigurationFromFile();
1411 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1412 }
648512ce
JB
1413 if (!Utils.isNullOrUndefined(configuration)) {
1414 delete configuration.stationInfo;
1415 delete configuration.configurationHash;
1416 }
073bd098 1417 return configuration;
7dde0b73
JB
1418 }
1419
551e477c
JB
1420 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1421 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1422 this.getOcppConfigurationFromFile();
2484ac1e
JB
1423 if (!ocppConfiguration) {
1424 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1425 }
1426 return ocppConfiguration;
1427 }
1428
c0560973 1429 private async onOpen(): Promise<void> {
56eb297e 1430 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1431 logger.info(
1432 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1433 );
ed6cfcff 1434 if (this.isRegistered() === false) {
5144f4d1
JB
1435 // Send BootNotification
1436 let registrationRetryCount = 0;
1437 do {
f7f98c68 1438 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1439 BootNotificationRequest,
1440 BootNotificationResponse
8bfbc743
JB
1441 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1442 skipBufferingOnError: true,
1443 });
ed6cfcff 1444 if (this.isRegistered() === false) {
5144f4d1
JB
1445 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1446 await Utils.sleep(
1895299d 1447 this?.bootNotificationResponse?.interval
5144f4d1
JB
1448 ? this.bootNotificationResponse.interval * 1000
1449 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1450 );
1451 }
1452 } while (
ed6cfcff 1453 this.isRegistered() === false &&
5144f4d1
JB
1454 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1455 this.getRegistrationMaxRetries() === -1)
1456 );
1457 }
ed6cfcff 1458 if (this.isRegistered() === true) {
23290150 1459 if (this.isInAcceptedState() === true) {
94bb24d5 1460 await this.startMessageSequence();
c0560973 1461 }
5144f4d1
JB
1462 } else {
1463 logger.error(
1464 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1465 );
caad9d6b 1466 }
5144f4d1 1467 this.wsConnectionRestarted = false;
aa428a31 1468 this.autoReconnectRetryCount = 0;
1895299d 1469 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1470 } else {
5144f4d1
JB
1471 logger.warn(
1472 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1473 );
2e6f5966 1474 }
2e6f5966
JB
1475 }
1476
ef7d8c21 1477 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1478 switch (code) {
6c65a295
JB
1479 // Normal close
1480 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1481 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1482 logger.info(
5e3cb728 1483 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1484 code
ef7d8c21 1485 )}' and reason '${reason.toString()}'`
e7aeea18 1486 );
c0560973
JB
1487 this.autoReconnectRetryCount = 0;
1488 break;
6c65a295
JB
1489 // Abnormal close
1490 default:
e7aeea18 1491 logger.error(
5e3cb728 1492 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1493 code
ef7d8c21 1494 )}' and reason '${reason.toString()}'`
e7aeea18 1495 );
56eb297e 1496 this.started === true && (await this.reconnect());
c0560973
JB
1497 break;
1498 }
1895299d 1499 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1500 }
1501
56d09fd7
JB
1502 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1503 const cachedRequest = this.requests.get(messageId);
1504 if (Array.isArray(cachedRequest) === true) {
1505 return cachedRequest;
1506 }
1507 throw new OCPPError(
1508 ErrorType.PROTOCOL_ERROR,
1509 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1510 messageType
1511 )} is not an array`,
1512 undefined,
617cad0c 1513 cachedRequest as JsonType
56d09fd7
JB
1514 );
1515 }
1516
1517 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1518 const [messageType, messageId, commandName, commandPayload] = request;
1519 if (this.getEnableStatistics() === true) {
1520 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1521 }
1522 logger.debug(
1523 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1524 request
1525 )}`
1526 );
1527 // Process the message
1528 await this.ocppIncomingRequestService.incomingRequestHandler(
1529 this,
1530 messageId,
1531 commandName,
1532 commandPayload
1533 );
1534 }
1535
1536 private handleResponseMessage(response: Response): void {
1537 const [messageType, messageId, commandPayload] = response;
1538 if (this.requests.has(messageId) === false) {
1539 // Error
1540 throw new OCPPError(
1541 ErrorType.INTERNAL_ERROR,
1542 `Response for unknown message id ${messageId}`,
1543 undefined,
1544 commandPayload
1545 );
1546 }
1547 // Respond
1548 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1549 messageType,
1550 messageId
1551 );
1552 logger.debug(
1553 `${this.logPrefix()} << Command '${
1554 requestCommandName ?? Constants.UNKNOWN_COMMAND
1555 }' received response payload: ${JSON.stringify(response)}`
1556 );
1557 responseCallback(commandPayload, requestPayload);
1558 }
1559
1560 private handleErrorMessage(errorResponse: ErrorResponse): void {
1561 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1562 if (this.requests.has(messageId) === false) {
1563 // Error
1564 throw new OCPPError(
1565 ErrorType.INTERNAL_ERROR,
1566 `Error response for unknown message id ${messageId}`,
1567 undefined,
1568 { errorType, errorMessage, errorDetails }
1569 );
1570 }
1571 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1572 logger.debug(
1573 `${this.logPrefix()} << Command '${
1574 requestCommandName ?? Constants.UNKNOWN_COMMAND
1575 }' received error response payload: ${JSON.stringify(errorResponse)}`
1576 );
1577 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1578 }
1579
ef7d8c21 1580 private async onMessage(data: RawData): Promise<void> {
56d09fd7 1581 let request: IncomingRequest | Response | ErrorResponse;
b3ec7bc1 1582 let messageType: number;
c0560973
JB
1583 let errMsg: string;
1584 try {
56d09fd7 1585 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1586 if (Array.isArray(request) === true) {
56d09fd7 1587 [messageType] = request;
b3ec7bc1
JB
1588 // Check the type of message
1589 switch (messageType) {
1590 // Incoming Message
1591 case MessageType.CALL_MESSAGE:
56d09fd7 1592 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1593 break;
56d09fd7 1594 // Response Message
b3ec7bc1 1595 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1596 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1597 break;
1598 // Error Message
1599 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1600 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1601 break;
56d09fd7 1602 // Unknown Message
b3ec7bc1
JB
1603 default:
1604 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fc040c43
JB
1605 errMsg = `Wrong message type ${messageType}`;
1606 logger.error(`${this.logPrefix()} ${errMsg}`);
b3ec7bc1
JB
1607 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1608 }
1895299d 1609 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1610 } else {
53e5fd67 1611 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1612 request,
ac54a9bb 1613 });
47e22477 1614 }
c0560973 1615 } catch (error) {
56d09fd7
JB
1616 let commandName: IncomingRequestCommand;
1617 let requestCommandName: RequestCommand | IncomingRequestCommand;
1618 let errorCallback: ErrorCallback;
1619 const [, messageId] = request;
13701f69
JB
1620 switch (messageType) {
1621 case MessageType.CALL_MESSAGE:
56d09fd7 1622 [, , commandName] = request as IncomingRequest;
13701f69 1623 // Send error
56d09fd7 1624 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1625 break;
1626 case MessageType.CALL_RESULT_MESSAGE:
1627 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7
JB
1628 if (this.requests.has(messageId) === true) {
1629 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
13701f69
JB
1630 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1631 errorCallback(error as OCPPError, false);
1632 } else {
1633 // Remove the request from the cache in case of error at response handling
1634 this.requests.delete(messageId);
1635 }
de4cb8b6 1636 break;
ba7965c4 1637 }
56d09fd7
JB
1638 if (error instanceof OCPPError === false) {
1639 logger.warn(
1640 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1641 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1642 }' message '${data.toString()}' handling is not an OCPPError:`,
1643 error
1644 );
1645 }
1646 logger.error(
1647 `${this.logPrefix()} Incoming OCPP command '${
1648 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1649 }' message '${data.toString()}'${
1650 messageType !== MessageType.CALL_MESSAGE
1651 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1652 : ''
1653 } processing error:`,
1654 error
1655 );
c0560973 1656 }
2328be1e
JB
1657 }
1658
c0560973 1659 private onPing(): void {
44eb6026 1660 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1661 }
1662
1663 private onPong(): void {
44eb6026 1664 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1665 }
1666
9534e74e 1667 private onError(error: WSError): void {
bcc9c3c0 1668 this.closeWSConnection();
44eb6026 1669 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1670 }
1671
18bf8274 1672 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 1673 if (this.getMeteringPerTransaction() === true) {
07989fad 1674 return (
18bf8274 1675 (rounded === true
07989fad
JB
1676 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1677 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1678 );
1679 }
1680 return (
18bf8274 1681 (rounded === true
07989fad
JB
1682 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1683 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1684 );
1685 }
1686
bb83b5ed 1687 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
fa7bccf4 1688 const localStationInfo = stationInfo ?? this.stationInfo;
a14885a3 1689 return localStationInfo?.useConnectorId0 ?? true;
8bce55bf
JB
1690 }
1691
60ddad53
JB
1692 private getNumberOfRunningTransactions(): number {
1693 let trxCount = 0;
1694 for (const connectorId of this.connectors.keys()) {
1695 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1696 trxCount++;
1697 }
1698 }
1699 return trxCount;
1700 }
1701
1702 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1703 for (const connectorId of this.connectors.keys()) {
1704 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1705 await this.stopTransactionOnConnector(connectorId, reason);
1706 }
1707 }
1708 }
1709
1f761b9a 1710 // 0 for disabling
c72f6634 1711 private getConnectionTimeout(): number {
17ac262c
JB
1712 if (
1713 ChargingStationConfigurationUtils.getConfigurationKey(
1714 this,
1715 StandardParametersKey.ConnectionTimeOut
1716 )
1717 ) {
e7aeea18 1718 return (
17ac262c
JB
1719 parseInt(
1720 ChargingStationConfigurationUtils.getConfigurationKey(
1721 this,
1722 StandardParametersKey.ConnectionTimeOut
1723 ).value
1724 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 1725 );
291cb255 1726 }
291cb255 1727 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1728 }
1729
1f761b9a 1730 // -1 for unlimited, 0 for disabling
72092cfc 1731 private getAutoReconnectMaxRetries(): number | undefined {
ad2f27c3
JB
1732 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1733 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1734 }
1735 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1736 return Configuration.getAutoReconnectMaxRetries();
1737 }
1738 return -1;
1739 }
1740
ec977daf 1741 // 0 for disabling
72092cfc 1742 private getRegistrationMaxRetries(): number | undefined {
ad2f27c3
JB
1743 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1744 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1745 }
1746 return -1;
1747 }
1748
c0560973
JB
1749 private getPowerDivider(): number {
1750 let powerDivider = this.getNumberOfConnectors();
fa7bccf4 1751 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 1752 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1753 }
1754 return powerDivider;
1755 }
1756
fa7bccf4
JB
1757 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1758 const maximumPower = this.getMaximumPower(stationInfo);
1759 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
1760 case CurrentType.AC:
1761 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 1762 this.getNumberOfPhases(stationInfo),
ad8537a7 1763 maximumPower / this.getNumberOfConnectors(),
fa7bccf4 1764 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
1765 );
1766 case CurrentType.DC:
fa7bccf4 1767 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
1768 }
1769 }
1770
cc6e8ab5
JB
1771 private getAmperageLimitation(): number | undefined {
1772 if (
5a2a53cf 1773 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1774 ChargingStationConfigurationUtils.getConfigurationKey(
1775 this,
1776 this.stationInfo.amperageLimitationOcppKey
1777 )
cc6e8ab5
JB
1778 ) {
1779 return (
1780 Utils.convertToInt(
17ac262c
JB
1781 ChargingStationConfigurationUtils.getConfigurationKey(
1782 this,
1783 this.stationInfo.amperageLimitationOcppKey
72092cfc 1784 )?.value
17ac262c 1785 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
1786 );
1787 }
1788 }
1789
c0560973 1790 private async startMessageSequence(): Promise<void> {
b7f9e41d 1791 if (this.stationInfo?.autoRegister === true) {
f7f98c68 1792 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
1793 BootNotificationRequest,
1794 BootNotificationResponse
8bfbc743
JB
1795 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1796 skipBufferingOnError: true,
1797 });
6114e6f1 1798 }
136c90ba 1799 // Start WebSocket ping
c0560973 1800 this.startWebSocketPing();
5ad8570f 1801 // Start heartbeat
c0560973 1802 this.startHeartbeat();
0a60c33c 1803 // Initialize connectors status
734d790d 1804 for (const connectorId of this.connectors.keys()) {
72092cfc 1805 let connectorStatus: ConnectorStatusEnum | undefined;
734d790d 1806 if (connectorId === 0) {
593cf3f9 1807 continue;
e7aeea18 1808 } else if (
56eb297e
JB
1809 !this.getConnectorStatus(connectorId)?.status &&
1810 (this.isChargingStationAvailable() === false ||
1789ba2c 1811 this.isConnectorAvailable(connectorId) === false)
e7aeea18 1812 ) {
721646e9 1813 connectorStatus = ConnectorStatusEnum.Unavailable;
45c0ae82
JB
1814 } else if (
1815 !this.getConnectorStatus(connectorId)?.status &&
1816 this.getConnectorStatus(connectorId)?.bootStatus
1817 ) {
1818 // Set boot status in template at startup
72092cfc 1819 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
56eb297e
JB
1820 } else if (this.getConnectorStatus(connectorId)?.status) {
1821 // Set previous status at startup
72092cfc 1822 connectorStatus = this.getConnectorStatus(connectorId)?.status;
5ad8570f 1823 } else {
56eb297e 1824 // Set default status
721646e9 1825 connectorStatus = ConnectorStatusEnum.Available;
5ad8570f 1826 }
56eb297e
JB
1827 await this.ocppRequestService.requestHandler<
1828 StatusNotificationRequest,
1829 StatusNotificationResponse
6e939d9e
JB
1830 >(
1831 this,
1832 RequestCommand.STATUS_NOTIFICATION,
1833 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1834 );
1835 this.getConnectorStatus(connectorId).status = connectorStatus;
5ad8570f 1836 }
c9a4f9ea
JB
1837 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1838 await this.ocppRequestService.requestHandler<
1839 FirmwareStatusNotificationRequest,
1840 FirmwareStatusNotificationResponse
1841 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1842 status: FirmwareStatus.Installed,
1843 });
1844 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 1845 }
3637ca2c 1846
0a60c33c 1847 // Start the ATG
60ddad53 1848 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
4f69be04 1849 this.startAutomaticTransactionGenerator();
fa7bccf4 1850 }
aa428a31 1851 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
1852 }
1853
e7aeea18
JB
1854 private async stopMessageSequence(
1855 reason: StopTransactionReason = StopTransactionReason.NONE
1856 ): Promise<void> {
136c90ba 1857 // Stop WebSocket ping
c0560973 1858 this.stopWebSocketPing();
79411696 1859 // Stop heartbeat
c0560973 1860 this.stopHeartbeat();
fa7bccf4 1861 // Stop ongoing transactions
b20eb107 1862 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
1863 this.stopAutomaticTransactionGenerator();
1864 } else {
1865 await this.stopRunningTransactions(reason);
79411696 1866 }
45c0ae82
JB
1867 for (const connectorId of this.connectors.keys()) {
1868 if (connectorId > 0) {
1869 await this.ocppRequestService.requestHandler<
1870 StatusNotificationRequest,
1871 StatusNotificationResponse
6e939d9e
JB
1872 >(
1873 this,
1874 RequestCommand.STATUS_NOTIFICATION,
1875 OCPPServiceUtils.buildStatusNotificationRequest(
1876 this,
1877 connectorId,
721646e9 1878 ConnectorStatusEnum.Unavailable
6e939d9e
JB
1879 )
1880 );
cdde2cfe 1881 delete this.getConnectorStatus(connectorId)?.status;
45c0ae82
JB
1882 }
1883 }
79411696
JB
1884 }
1885
c0560973 1886 private startWebSocketPing(): void {
17ac262c
JB
1887 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1888 this,
e7aeea18
JB
1889 StandardParametersKey.WebSocketPingInterval
1890 )
1891 ? Utils.convertToInt(
17ac262c
JB
1892 ChargingStationConfigurationUtils.getConfigurationKey(
1893 this,
1894 StandardParametersKey.WebSocketPingInterval
72092cfc 1895 )?.value
e7aeea18 1896 )
9cd3dfb0 1897 : 0;
ad2f27c3
JB
1898 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1899 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 1900 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 1901 this.wsConnection?.ping();
136c90ba
JB
1902 }
1903 }, webSocketPingInterval * 1000);
e7aeea18 1904 logger.info(
44eb6026
JB
1905 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
1906 webSocketPingInterval
1907 )}`
e7aeea18 1908 );
ad2f27c3 1909 } else if (this.webSocketPingSetInterval) {
e7aeea18 1910 logger.info(
44eb6026
JB
1911 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
1912 webSocketPingInterval
1913 )}`
e7aeea18 1914 );
136c90ba 1915 } else {
e7aeea18
JB
1916 logger.error(
1917 `${this.logPrefix()} WebSocket ping interval set to ${
1918 webSocketPingInterval
1919 ? Utils.formatDurationSeconds(webSocketPingInterval)
1920 : webSocketPingInterval
1921 }, not starting the WebSocket ping`
1922 );
136c90ba
JB
1923 }
1924 }
1925
c0560973 1926 private stopWebSocketPing(): void {
ad2f27c3
JB
1927 if (this.webSocketPingSetInterval) {
1928 clearInterval(this.webSocketPingSetInterval);
136c90ba
JB
1929 }
1930 }
1931
1f5df42a 1932 private getConfiguredSupervisionUrl(): URL {
72092cfc 1933 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
53ac516c 1934 if (Utils.isNotEmptyArray(supervisionUrls)) {
2dcfe98e 1935 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 1936 case SupervisionUrlDistribution.RANDOM:
c72f6634
JB
1937 this.configuredSupervisionUrlIndex = Math.floor(
1938 Utils.secureRandom() * supervisionUrls.length
1939 );
2dcfe98e 1940 break;
a52a6446 1941 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 1942 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 1943 default:
a52a6446
JB
1944 Object.values(SupervisionUrlDistribution).includes(
1945 Configuration.getSupervisionUrlDistribution()
1946 ) === false &&
1947 logger.error(
1948 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1949 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
1950 }`
1951 );
c72f6634 1952 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 1953 break;
c0560973 1954 }
c72f6634 1955 return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
c0560973 1956 }
57939a9d 1957 return new URL(supervisionUrls as string);
136c90ba
JB
1958 }
1959
c72f6634 1960 private getHeartbeatInterval(): number {
17ac262c
JB
1961 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1962 this,
1963 StandardParametersKey.HeartbeatInterval
1964 );
c0560973
JB
1965 if (HeartbeatInterval) {
1966 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1967 }
17ac262c
JB
1968 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1969 this,
1970 StandardParametersKey.HeartBeatInterval
1971 );
c0560973
JB
1972 if (HeartBeatInterval) {
1973 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
0a60c33c 1974 }
b7f9e41d 1975 this.stationInfo?.autoRegister === false &&
e7aeea18
JB
1976 logger.warn(
1977 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1978 Constants.DEFAULT_HEARTBEAT_INTERVAL
1979 }`
1980 );
47e22477 1981 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
0a60c33c
JB
1982 }
1983
c0560973 1984 private stopHeartbeat(): void {
ad2f27c3
JB
1985 if (this.heartbeatSetInterval) {
1986 clearInterval(this.heartbeatSetInterval);
7dde0b73 1987 }
5ad8570f
JB
1988 }
1989
55516218 1990 private terminateWSConnection(): void {
56eb297e 1991 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 1992 this.wsConnection?.terminate();
55516218
JB
1993 this.wsConnection = null;
1994 }
1995 }
1996
dd119a6b 1997 private stopMeterValues(connectorId: number) {
734d790d 1998 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
72092cfc 1999 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
dd119a6b
JB
2000 }
2001 }
2002
c72f6634 2003 private getReconnectExponentialDelay(): boolean {
a14885a3 2004 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2005 }
2006
aa428a31 2007 private async reconnect(): Promise<void> {
7874b0b1
JB
2008 // Stop WebSocket ping
2009 this.stopWebSocketPing();
136c90ba 2010 // Stop heartbeat
c0560973 2011 this.stopHeartbeat();
5ad8570f 2012 // Stop the ATG if needed
6d9876e7 2013 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
fa7bccf4 2014 this.stopAutomaticTransactionGenerator();
ad2f27c3 2015 }
e7aeea18
JB
2016 if (
2017 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2018 this.getAutoReconnectMaxRetries() === -1
2019 ) {
ad2f27c3 2020 this.autoReconnectRetryCount++;
e7aeea18
JB
2021 const reconnectDelay = this.getReconnectExponentialDelay()
2022 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2023 : this.getConnectionTimeout() * 1000;
1e080116
JB
2024 const reconnectDelayWithdraw = 1000;
2025 const reconnectTimeout =
2026 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2027 ? reconnectDelay - reconnectDelayWithdraw
2028 : 0;
e7aeea18 2029 logger.error(
d56ea27c 2030 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2031 reconnectDelay,
2032 2
2033 )}ms, timeout ${reconnectTimeout}ms`
2034 );
032d6efc 2035 await Utils.sleep(reconnectDelay);
e7aeea18 2036 logger.error(
44eb6026 2037 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
e7aeea18
JB
2038 );
2039 this.openWSConnection(
59b6ed8d
JB
2040 {
2041 ...(this.stationInfo?.wsOptions ?? Constants.EMPTY_OBJECT),
2042 handshakeTimeout: reconnectTimeout,
2043 },
1e080116 2044 { closeOpened: true }
e7aeea18 2045 );
265e4266 2046 this.wsConnectionRestarted = true;
c0560973 2047 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2048 logger.error(
d56ea27c 2049 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2050 this.autoReconnectRetryCount
d56ea27c 2051 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2052 );
5ad8570f
JB
2053 }
2054 }
2055
551e477c
JB
2056 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2057 | AutomaticTransactionGeneratorConfiguration
2058 | undefined {
2059 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
fa7bccf4
JB
2060 }
2061
a2653482
JB
2062 private initializeConnectorStatus(connectorId: number): void {
2063 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2064 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2065 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d
JB
2066 this.getConnectorStatus(connectorId).transactionStarted = false;
2067 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2068 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
0a60c33c 2069 }
7dde0b73 2070}