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