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