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