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