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