fix: ensure stop transaction is sent at ATG stop
[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,
fa5995d6 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
fa5995d6 667 this.templateFileWatcher = watchJsonFile(
0d8852a5 668 this.templateFile,
7164966d
JB
669 FileType.ChargingStationTemplate,
670 this.logPrefix(),
671 undefined,
0d8852a5 672 (event, filename): void => {
9bf0ef23 673 if (isNotEmptyString(filename) && event === 'change') {
0d8852a5
JB
674 try {
675 logger.debug(
676 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
677 this.templateFile
5edd8ba0 678 } file have changed, reload`,
0d8852a5 679 );
cda5d0fb 680 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
0d8852a5
JB
681 // Initialize
682 this.initialize();
e1d9a0f4 683 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!);
0d8852a5 684 // Restart the ATG
49563992 685 this.stopAutomaticTransactionGenerator().catch(() => {});
e3d35515 686 delete this.automaticTransactionGeneratorConfiguration;
ac7f79af 687 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
0d8852a5
JB
688 this.startAutomaticTransactionGenerator();
689 }
ad774cec 690 if (this.getEnableStatistics() === true) {
551e477c 691 this.performanceStatistics?.restart();
0d8852a5 692 } else {
551e477c 693 this.performanceStatistics?.stop();
0d8852a5
JB
694 }
695 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
696 } catch (error) {
697 logger.error(
698 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
5edd8ba0 699 error,
0d8852a5 700 );
950b1349 701 }
a95873d8 702 }
5edd8ba0 703 },
0d8852a5 704 );
56eb297e 705 this.started = true;
c8faabc8 706 parentPort?.postMessage(buildStartedMessage(this));
0d8852a5
JB
707 this.starting = false;
708 } else {
709 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
710 }
950b1349 711 } else {
0d8852a5 712 logger.warn(`${this.logPrefix()} Charging station is already started...`);
950b1349 713 }
c0560973
JB
714 }
715
60ddad53 716 public async stop(reason?: StopTransactionReason): Promise<void> {
0d8852a5
JB
717 if (this.started === true) {
718 if (this.stopping === false) {
719 this.stopping = true;
720 await this.stopMessageSequence(reason);
0d8852a5 721 this.closeWSConnection();
ad774cec 722 if (this.getEnableStatistics() === true) {
551e477c 723 this.performanceStatistics?.stop();
0d8852a5 724 }
d8093be1 725 if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
5543b88d
JB
726 this.stopReservationExpirationSetInterval();
727 }
0d8852a5 728 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
72092cfc 729 this.templateFileWatcher?.close();
cda5d0fb 730 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
cdde2cfe 731 delete this.bootNotificationResponse;
0d8852a5 732 this.started = false;
ac7f79af 733 this.saveConfiguration();
c8faabc8 734 parentPort?.postMessage(buildStoppedMessage(this));
0d8852a5
JB
735 this.stopping = false;
736 } else {
737 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
c0560973 738 }
950b1349 739 } else {
0d8852a5 740 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
c0560973 741 }
c0560973
JB
742 }
743
60ddad53
JB
744 public async reset(reason?: StopTransactionReason): Promise<void> {
745 await this.stop(reason);
e1d9a0f4 746 await sleep(this.stationInfo.resetTime!);
fa7bccf4 747 this.initialize();
94ec7e96
JB
748 this.start();
749 }
750
17ac262c
JB
751 public saveOcppConfiguration(): void {
752 if (this.getOcppPersistentConfiguration()) {
b1bbdae5 753 this.saveConfiguration();
e6895390
JB
754 }
755 }
756
8e242273
JB
757 public bufferMessage(message: string): void {
758 this.messageBuffer.add(message);
3ba2381e
JB
759 }
760
db2336d9 761 public openWSConnection(
7f3decca
JB
762 options?: WsOptions,
763 params?: { closeOpened?: boolean; terminateOpened?: boolean },
db2336d9 764 ): void {
7f3decca
JB
765 options = {
766 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
e6a33233 767 ...this.stationInfo?.wsOptions,
7f3decca
JB
768 ...options,
769 };
b1bbdae5 770 params = { ...{ closeOpened: false, terminateOpened: false }, ...params };
e6a33233 771 if (!checkChargingStation(this, this.logPrefix())) {
d1c6c833
JB
772 return;
773 }
db2336d9 774 if (
9bf0ef23
JB
775 !isNullOrUndefined(this.stationInfo.supervisionUser) &&
776 !isNullOrUndefined(this.stationInfo.supervisionPassword)
db2336d9
JB
777 ) {
778 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
779 }
780 if (params?.closeOpened) {
781 this.closeWSConnection();
782 }
783 if (params?.terminateOpened) {
784 this.terminateWSConnection();
785 }
db2336d9 786
56eb297e 787 if (this.isWebSocketConnectionOpened() === true) {
0a03f36c 788 logger.warn(
944d4529 789 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`,
0a03f36c
JB
790 );
791 return;
792 }
793
db2336d9 794 logger.info(
5edd8ba0 795 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`,
db2336d9
JB
796 );
797
feff11ec
JB
798 this.wsConnection = new WebSocket(
799 this.wsConnectionUrl,
800 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
5edd8ba0 801 options,
feff11ec 802 );
db2336d9
JB
803
804 // Handle WebSocket message
805 this.wsConnection.on(
806 'message',
5edd8ba0 807 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void,
db2336d9
JB
808 );
809 // Handle WebSocket error
810 this.wsConnection.on(
811 'error',
5edd8ba0 812 this.onError.bind(this) as (this: WebSocket, error: Error) => void,
db2336d9
JB
813 );
814 // Handle WebSocket close
815 this.wsConnection.on(
816 'close',
5edd8ba0 817 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void,
db2336d9
JB
818 );
819 // Handle WebSocket open
820 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
821 // Handle WebSocket ping
822 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
823 // Handle WebSocket pong
824 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
825 }
826
827 public closeWSConnection(): void {
56eb297e 828 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 829 this.wsConnection?.close();
db2336d9
JB
830 this.wsConnection = null;
831 }
832 }
833
e1d9a0f4 834 public getAutomaticTransactionGeneratorConfiguration(): AutomaticTransactionGeneratorConfiguration {
c7db8ecb
JB
835 if (isNullOrUndefined(this.automaticTransactionGeneratorConfiguration)) {
836 let automaticTransactionGeneratorConfiguration:
837 | AutomaticTransactionGeneratorConfiguration
838 | undefined;
839 const automaticTransactionGeneratorConfigurationFromFile =
840 this.getConfigurationFromFile()?.automaticTransactionGenerator;
841 if (
842 this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
843 automaticTransactionGeneratorConfigurationFromFile
844 ) {
845 automaticTransactionGeneratorConfiguration =
846 automaticTransactionGeneratorConfigurationFromFile;
847 } else {
848 automaticTransactionGeneratorConfiguration =
849 this.getTemplateFromFile()?.AutomaticTransactionGenerator;
850 }
851 this.automaticTransactionGeneratorConfiguration = {
852 ...Constants.DEFAULT_ATG_CONFIGURATION,
853 ...automaticTransactionGeneratorConfiguration,
854 };
ac7f79af 855 }
c7db8ecb 856 return this.automaticTransactionGeneratorConfiguration!;
ac7f79af
JB
857 }
858
5ced7e80
JB
859 public getAutomaticTransactionGeneratorStatuses(): Status[] | undefined {
860 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses;
861 }
862
ac7f79af
JB
863 public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
864 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
9bf0ef23 865 if (isNotEmptyArray(connectorIds)) {
e1d9a0f4 866 for (const connectorId of connectorIds!) {
551e477c 867 this.automaticTransactionGenerator?.startConnector(connectorId);
a5e9befc
JB
868 }
869 } else {
551e477c 870 this.automaticTransactionGenerator?.start();
4f69be04 871 }
cb60061f 872 this.saveAutomaticTransactionGeneratorConfiguration();
c8faabc8 873 parentPort?.postMessage(buildUpdatedMessage(this));
4f69be04
JB
874 }
875
49563992 876 public async stopAutomaticTransactionGenerator(connectorIds?: number[]): Promise<void> {
9bf0ef23 877 if (isNotEmptyArray(connectorIds)) {
e1d9a0f4 878 for (const connectorId of connectorIds!) {
49563992 879 await this.automaticTransactionGenerator?.stopConnector(connectorId);
a5e9befc
JB
880 }
881 } else {
49563992 882 await this.automaticTransactionGenerator?.stop();
4f69be04 883 }
cb60061f 884 this.saveAutomaticTransactionGeneratorConfiguration();
c8faabc8 885 parentPort?.postMessage(buildUpdatedMessage(this));
4f69be04
JB
886 }
887
5e3cb728
JB
888 public async stopTransactionOnConnector(
889 connectorId: number,
5edd8ba0 890 reason = StopTransactionReason.NONE,
5e3cb728 891 ): Promise<StopTransactionResponse> {
72092cfc 892 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
5e3cb728 893 if (
c7e8e0a2
JB
894 this.getBeginEndMeterValues() === true &&
895 this.getOcppStrictCompliance() === true &&
896 this.getOutOfOrderEndMeterValues() === false
5e3cb728
JB
897 ) {
898 // FIXME: Implement OCPP version agnostic helpers
899 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
900 this,
901 connectorId,
e1d9a0f4 902 this.getEnergyActiveImportRegisterByTransactionId(transactionId!),
5e3cb728
JB
903 );
904 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
905 this,
906 RequestCommand.METER_VALUES,
907 {
908 connectorId,
909 transactionId,
910 meterValue: [transactionEndMeterValue],
5edd8ba0 911 },
5e3cb728
JB
912 );
913 }
914 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
915 this,
916 RequestCommand.STOP_TRANSACTION,
917 {
918 transactionId,
e1d9a0f4 919 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
5e3cb728 920 reason,
5edd8ba0 921 },
5e3cb728
JB
922 );
923 }
924
10e8c3e1 925 public getReserveConnectorZeroSupported(): boolean {
9bf0ef23 926 return convertToBoolean(
f2d5e3d9 927 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value,
24578c31
JB
928 );
929 }
930
d193a949 931 public async addReservation(reservation: Reservation): Promise<void> {
2ca0ea90 932 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId);
530e5fbb
JB
933 if (!isUndefined(reservationFound)) {
934 await this.removeReservation(
935 reservationFound!,
936 ReservationTerminationReason.REPLACE_EXISTING,
937 );
d193a949 938 }
e1d9a0f4 939 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation;
ec94a3cf 940 await OCPPServiceUtils.sendAndSetConnectorStatus(
d193a949 941 this,
ec94a3cf
JB
942 reservation.connectorId,
943 ConnectorStatusEnum.Reserved,
e1d9a0f4 944 undefined,
5edd8ba0 945 { send: reservation.connectorId !== 0 },
d193a949 946 );
24578c31
JB
947 }
948
d193a949
JB
949 public async removeReservation(
950 reservation: Reservation,
90aceaf6 951 reason: ReservationTerminationReason,
d193a949 952 ): Promise<void> {
e1d9a0f4 953 const connector = this.getConnectorStatus(reservation.connectorId)!;
d193a949 954 switch (reason) {
96d96b12 955 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
ec94a3cf 956 case ReservationTerminationReason.TRANSACTION_STARTED:
ec9f36cc
JB
957 delete connector.reservation;
958 break;
e74bc549
JB
959 case ReservationTerminationReason.RESERVATION_CANCELED:
960 case ReservationTerminationReason.REPLACE_EXISTING:
961 case ReservationTerminationReason.EXPIRED:
ec94a3cf 962 await OCPPServiceUtils.sendAndSetConnectorStatus(
d193a949 963 this,
ec94a3cf
JB
964 reservation.connectorId,
965 ConnectorStatusEnum.Available,
e1d9a0f4 966 undefined,
5edd8ba0 967 { send: reservation.connectorId !== 0 },
d193a949 968 );
ec94a3cf 969 delete connector.reservation;
d193a949 970 break;
b029e74e 971 default:
90aceaf6 972 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
56563a3c 973 throw new BaseError(`Unknown reservation termination reason '${reason}'`);
d193a949 974 }
24578c31
JB
975 }
976
3fa7f799 977 public getReservationBy(
366f75f6 978 filterKey: ReservationKey,
5edd8ba0 979 value: number | string,
3fa7f799 980 ): Reservation | undefined {
66dd3447 981 if (this.hasEvses) {
3fa7f799
JB
982 for (const evseStatus of this.evses.values()) {
983 for (const connectorStatus of evseStatus.connectors.values()) {
2ca0ea90 984 if (connectorStatus?.reservation?.[filterKey] === value) {
3fa7f799 985 return connectorStatus.reservation;
66dd3447
JB
986 }
987 }
988 }
989 } else {
3fa7f799 990 for (const connectorStatus of this.connectors.values()) {
2ca0ea90 991 if (connectorStatus?.reservation?.[filterKey] === value) {
3fa7f799 992 return connectorStatus.reservation;
66dd3447
JB
993 }
994 }
995 }
d193a949
JB
996 }
997
e6948a57
JB
998 public isConnectorReservable(
999 reservationId: number,
1000 idTag?: string,
1001 connectorId?: number,
1002 ): boolean {
90aceaf6
JB
1003 const reservation = this.getReservationBy('reservationId', reservationId);
1004 const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!);
e6948a57
JB
1005 if (arguments.length === 1) {
1006 return !reservationExists;
1007 } else if (arguments.length > 1) {
90aceaf6
JB
1008 const userReservation = !isUndefined(idTag)
1009 ? this.getReservationBy('idTag', idTag!)
1010 : undefined;
e6948a57 1011 const userReservationExists =
90aceaf6 1012 !isUndefined(userReservation) && !hasReservationExpired(userReservation!);
e6948a57
JB
1013 const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
1014 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
1015 return (
1016 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
1017 );
1018 }
1019 return false;
1020 }
1021
1022 private startReservationExpirationSetInterval(customInterval?: number): void {
2035255d 1023 const interval = customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_INTERVAL;
42371a2e 1024 if (interval > 0) {
04c32a95
JB
1025 logger.info(
1026 `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
1027 interval,
1028 )}`,
1029 );
37aa4e56 1030 this.reservationExpirationSetInterval = setInterval((): void => {
90aceaf6 1031 removeExpiredReservations(this).catch(Constants.EMPTY_FUNCTION);
42371a2e
JB
1032 }, interval);
1033 }
d193a949
JB
1034 }
1035
e6948a57 1036 private stopReservationExpirationSetInterval(): void {
d116b9b5 1037 if (!isNullOrUndefined(this.reservationExpirationSetInterval)) {
e6948a57
JB
1038 clearInterval(this.reservationExpirationSetInterval);
1039 }
d193a949
JB
1040 }
1041
366f75f6
JB
1042 // private restartReservationExpiryDateSetInterval(): void {
1043 // this.stopReservationExpirationSetInterval();
1044 // this.startReservationExpirationSetInterval();
1045 // }
d193a949
JB
1046
1047 private getNumberOfReservableConnectors(): number {
90aceaf6 1048 let numberOfReservableConnectors = 0;
66dd3447 1049 if (this.hasEvses) {
3fa7f799 1050 for (const evseStatus of this.evses.values()) {
90aceaf6 1051 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors);
66dd3447
JB
1052 }
1053 } else {
90aceaf6 1054 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors);
66dd3447 1055 }
90aceaf6 1056 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero();
66dd3447
JB
1057 }
1058
d193a949 1059 private getNumberOfReservationsOnConnectorZero(): number {
6913d568
JB
1060 if (
1061 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1062 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) ||
1063 (!this.hasEvses && this.connectors.get(0)?.reservation)
1064 ) {
cfc9875a 1065 return 1;
66dd3447 1066 }
cfc9875a 1067 return 0;
24578c31
JB
1068 }
1069
f90c1757 1070 private flushMessageBuffer(): void {
8e242273 1071 if (this.messageBuffer.size > 0) {
7d3b0f64 1072 for (const message of this.messageBuffer.values()) {
e1d9a0f4
JB
1073 let beginId: string | undefined;
1074 let commandName: RequestCommand | undefined;
8ca6874c 1075 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
1076 const isRequest = messageType === MessageType.CALL_MESSAGE;
1077 if (isRequest) {
1078 [, , commandName] = JSON.parse(message) as OutgoingRequest;
1079 beginId = PerformanceStatistics.beginMeasure(commandName);
1080 }
72092cfc 1081 this.wsConnection?.send(message);
e1d9a0f4 1082 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!);
8ca6874c
JB
1083 logger.debug(
1084 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0
JB
1085 messageType,
1086 )} payload sent: ${message}`,
8ca6874c 1087 );
8e242273 1088 this.messageBuffer.delete(message);
7d3b0f64 1089 }
77f00f84
JB
1090 }
1091 }
1092
1f5df42a
JB
1093 private getSupervisionUrlOcppConfiguration(): boolean {
1094 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
1095 }
1096
e8e865ea 1097 private getSupervisionUrlOcppKey(): string {
6dad8e21 1098 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
e8e865ea
JB
1099 }
1100
72092cfc 1101 private getTemplateFromFile(): ChargingStationTemplate | undefined {
e1d9a0f4 1102 let template: ChargingStationTemplate | undefined;
5ad8570f 1103 try {
cda5d0fb
JB
1104 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1105 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash);
7c72977b
JB
1106 } else {
1107 const measureId = `${FileType.ChargingStationTemplate} read`;
1108 const beginId = PerformanceStatistics.beginMeasure(measureId);
d972af76 1109 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
7c72977b 1110 PerformanceStatistics.endMeasure(measureId, beginId);
d972af76 1111 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
7c72977b
JB
1112 .update(JSON.stringify(template))
1113 .digest('hex');
57adbebc 1114 this.sharedLRUCache.setChargingStationTemplate(template);
cda5d0fb 1115 this.templateFileHash = template.templateHash;
7c72977b 1116 }
5ad8570f 1117 } catch (error) {
fa5995d6 1118 handleFileException(
2484ac1e 1119 this.templateFile,
7164966d
JB
1120 FileType.ChargingStationTemplate,
1121 error as NodeJS.ErrnoException,
5edd8ba0 1122 this.logPrefix(),
e7aeea18 1123 );
5ad8570f 1124 }
2484ac1e
JB
1125 return template;
1126 }
1127
7a3a2ebb 1128 private getStationInfoFromTemplate(): ChargingStationInfo {
e1d9a0f4 1129 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
fba11dc6 1130 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
5f742aac
JB
1131 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this);
1132 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile);
8a133cc8 1133 if (stationTemplate?.Connectors) {
fba11dc6 1134 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
8a133cc8 1135 }
fba11dc6
JB
1136 const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate);
1137 stationInfo.hashId = getHashId(this.index, stationTemplate);
1138 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
72092cfc 1139 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fba11dc6 1140 createSerialNumber(stationTemplate, stationInfo);
9bf0ef23 1141 if (isNotEmptyArray(stationTemplate?.power)) {
551e477c 1142 stationTemplate.power = stationTemplate.power as number[];
9bf0ef23 1143 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
cc6e8ab5 1144 stationInfo.maximumPower =
72092cfc 1145 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1146 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1147 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 1148 } else {
551e477c 1149 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 1150 stationInfo.maximumPower =
72092cfc 1151 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
1152 ? stationTemplate.power * 1000
1153 : stationTemplate.power;
1154 }
3637ca2c 1155 stationInfo.firmwareVersionPattern =
72092cfc 1156 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 1157 if (
9bf0ef23 1158 isNotEmptyString(stationInfo.firmwareVersion) &&
e1d9a0f4 1159 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!) === false
3637ca2c
JB
1160 ) {
1161 logger.warn(
1162 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1163 this.templateFile
5edd8ba0 1164 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`,
3637ca2c
JB
1165 );
1166 }
598c886d 1167 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 1168 {
598c886d
JB
1169 versionUpgrade: {
1170 step: 1,
1171 },
15748260
JB
1172 reset: true,
1173 },
5edd8ba0 1174 stationTemplate?.firmwareUpgrade ?? {},
15748260 1175 );
9bf0ef23 1176 stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
be4c6702 1177 ? secondsToMilliseconds(stationTemplate.resetTime!)
e7aeea18 1178 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
fa7bccf4 1179 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
9ac86a7e 1180 return stationInfo;
5ad8570f
JB
1181 }
1182
551e477c
JB
1183 private getStationInfoFromFile(): ChargingStationInfo | undefined {
1184 let stationInfo: ChargingStationInfo | undefined;
f832e5df
JB
1185 if (this.getStationInfoPersistentConfiguration()) {
1186 stationInfo = this.getConfigurationFromFile()?.stationInfo;
1187 if (stationInfo) {
1188 delete stationInfo?.infoHash;
1189 }
1190 }
f765beaa 1191 return stationInfo;
2484ac1e
JB
1192 }
1193
1194 private getStationInfo(): ChargingStationInfo {
1195 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 1196 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
6b90dcca
JB
1197 // Priority:
1198 // 1. charging station info from template
1199 // 2. charging station info from configuration file
f765beaa 1200 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
e1d9a0f4 1201 return stationInfoFromFile!;
f765beaa 1202 }
fec4d204 1203 stationInfoFromFile &&
fba11dc6 1204 propagateSerialNumber(
e1d9a0f4 1205 this.getTemplateFromFile()!,
fec4d204 1206 stationInfoFromFile,
5edd8ba0 1207 stationInfoFromTemplate,
fec4d204 1208 );
01efc60a 1209 return stationInfoFromTemplate;
2484ac1e
JB
1210 }
1211
1212 private saveStationInfo(): void {
ccb1d6e9 1213 if (this.getStationInfoPersistentConfiguration()) {
b1bbdae5 1214 this.saveConfiguration();
ccb1d6e9 1215 }
2484ac1e
JB
1216 }
1217
e8e865ea 1218 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
1219 return this.stationInfo?.ocppPersistentConfiguration ?? true;
1220 }
1221
1222 private getStationInfoPersistentConfiguration(): boolean {
1223 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
1224 }
1225
5ced7e80
JB
1226 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1227 return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
1228 }
1229
c0560973 1230 private handleUnsupportedVersion(version: OCPPVersion) {
944d4529 1231 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
ded57f02
JB
1232 logger.error(`${this.logPrefix()} ${errorMsg}`);
1233 throw new BaseError(errorMsg);
c0560973
JB
1234 }
1235
2484ac1e 1236 private initialize(): void {
e1d9a0f4 1237 const stationTemplate = this.getTemplateFromFile()!;
fba11dc6 1238 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
d972af76
JB
1239 this.configurationFile = join(
1240 dirname(this.templateFile.replace('station-templates', 'configurations')),
5edd8ba0 1241 `${getHashId(this.index, stationTemplate)}.json`,
0642c3d2 1242 );
a4f7c75f 1243 const chargingStationConfiguration = this.getConfigurationFromFile();
a4f7c75f 1244 if (
ba01a213 1245 chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
e1d9a0f4 1246 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
a4f7c75f
JB
1247 (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
1248 ) {
1249 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1250 } else {
1251 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1252 }
b44b779a 1253 this.stationInfo = this.getStationInfo();
3637ca2c
JB
1254 if (
1255 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
9bf0ef23
JB
1256 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1257 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1258 ) {
d812bdcb 1259 const patternGroup: number | undefined =
15748260 1260 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1261 this.stationInfo.firmwareVersion?.split('.').length;
e1d9a0f4
JB
1262 const match = this.stationInfo
1263 .firmwareVersion!.match(new RegExp(this.stationInfo.firmwareVersionPattern!))!
1264 .slice(1, patternGroup! + 1);
3637ca2c 1265 const patchLevelIndex = match.length - 1;
5d280aae 1266 match[patchLevelIndex] = (
9bf0ef23 1267 convertToInt(match[patchLevelIndex]) +
e1d9a0f4 1268 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
5d280aae 1269 ).toString();
72092cfc 1270 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1271 }
6bccfcbc 1272 this.saveStationInfo();
6bccfcbc
JB
1273 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1274 if (this.getEnableStatistics() === true) {
1275 this.performanceStatistics = PerformanceStatistics.getInstance(
1276 this.stationInfo.hashId,
e1d9a0f4 1277 this.stationInfo.chargingStationId!,
5edd8ba0 1278 this.configuredSupervisionUrl,
6bccfcbc
JB
1279 );
1280 }
fba11dc6 1281 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
692f2f64
JB
1282 this.powerDivider = this.getPowerDivider();
1283 // OCPP configuration
1284 this.ocppConfiguration = this.getOcppConfiguration();
1285 this.initializeOcppConfiguration();
1286 this.initializeOcppServices();
1287 if (this.stationInfo?.autoRegister === true) {
1288 this.bootNotificationResponse = {
1289 currentTime: new Date(),
be4c6702 1290 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
692f2f64
JB
1291 status: RegistrationStatusEnumType.ACCEPTED,
1292 };
1293 }
147d0e0f
JB
1294 }
1295
feff11ec
JB
1296 private initializeOcppServices(): void {
1297 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1298 switch (ocppVersion) {
1299 case OCPPVersion.VERSION_16:
1300 this.ocppIncomingRequestService =
1301 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1302 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
5edd8ba0 1303 OCPP16ResponseService.getInstance<OCPP16ResponseService>(),
feff11ec
JB
1304 );
1305 break;
1306 case OCPPVersion.VERSION_20:
1307 case OCPPVersion.VERSION_201:
1308 this.ocppIncomingRequestService =
1309 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1310 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
5edd8ba0 1311 OCPP20ResponseService.getInstance<OCPP20ResponseService>(),
feff11ec
JB
1312 );
1313 break;
1314 default:
1315 this.handleUnsupportedVersion(ocppVersion);
1316 break;
1317 }
1318 }
1319
2484ac1e 1320 private initializeOcppConfiguration(): void {
f2d5e3d9
JB
1321 if (!getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)) {
1322 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0');
f0f65a62 1323 }
f2d5e3d9
JB
1324 if (!getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)) {
1325 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false });
f0f65a62 1326 }
e7aeea18
JB
1327 if (
1328 this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 1329 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
f2d5e3d9 1330 !getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1331 ) {
f2d5e3d9 1332 addConfigurationKey(
17ac262c 1333 this,
a59737e3 1334 this.getSupervisionUrlOcppKey(),
fa7bccf4 1335 this.configuredSupervisionUrl.href,
5edd8ba0 1336 { reboot: true },
e7aeea18 1337 );
e6895390
JB
1338 } else if (
1339 !this.getSupervisionUrlOcppConfiguration() &&
9bf0ef23 1340 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
f2d5e3d9 1341 getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1342 ) {
f2d5e3d9 1343 deleteConfigurationKey(this, this.getSupervisionUrlOcppKey(), { save: false });
12fc74d6 1344 }
cc6e8ab5 1345 if (
9bf0ef23 1346 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
f2d5e3d9 1347 !getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
cc6e8ab5 1348 ) {
f2d5e3d9 1349 addConfigurationKey(
17ac262c 1350 this,
e1d9a0f4 1351 this.stationInfo.amperageLimitationOcppKey!,
17ac262c 1352 (
e1d9a0f4 1353 this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)
5edd8ba0 1354 ).toString(),
cc6e8ab5
JB
1355 );
1356 }
f2d5e3d9
JB
1357 if (!getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)) {
1358 addConfigurationKey(
17ac262c 1359 this,
e7aeea18 1360 StandardParametersKey.SupportedFeatureProfiles,
5edd8ba0 1361 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
e7aeea18
JB
1362 );
1363 }
f2d5e3d9 1364 addConfigurationKey(
17ac262c 1365 this,
e7aeea18
JB
1366 StandardParametersKey.NumberOfConnectors,
1367 this.getNumberOfConnectors().toString(),
a95873d8 1368 { readonly: true },
5edd8ba0 1369 { overwrite: true },
e7aeea18 1370 );
f2d5e3d9
JB
1371 if (!getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData)) {
1372 addConfigurationKey(
17ac262c 1373 this,
e7aeea18 1374 StandardParametersKey.MeterValuesSampledData,
5edd8ba0 1375 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
e7aeea18 1376 );
7abfea5f 1377 }
f2d5e3d9 1378 if (!getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation)) {
dd08d43d 1379 const connectorsPhaseRotation: string[] = [];
28e78158
JB
1380 if (this.hasEvses) {
1381 for (const evseStatus of this.evses.values()) {
1382 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d 1383 connectorsPhaseRotation.push(
e1d9a0f4 1384 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1385 );
28e78158
JB
1386 }
1387 }
1388 } else {
1389 for (const connectorId of this.connectors.keys()) {
dd08d43d 1390 connectorsPhaseRotation.push(
e1d9a0f4 1391 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!,
dd08d43d 1392 );
7e1dc878
JB
1393 }
1394 }
f2d5e3d9 1395 addConfigurationKey(
17ac262c 1396 this,
e7aeea18 1397 StandardParametersKey.ConnectorPhaseRotation,
5edd8ba0 1398 connectorsPhaseRotation.toString(),
e7aeea18 1399 );
7e1dc878 1400 }
f2d5e3d9
JB
1401 if (!getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests)) {
1402 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
36f6a92e 1403 }
17ac262c 1404 if (
f2d5e3d9
JB
1405 !getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) &&
1406 getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles)?.value?.includes(
1407 SupportedFeatureProfiles.LocalAuthListManagement,
17ac262c
JB
1408 )
1409 ) {
f2d5e3d9
JB
1410 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false');
1411 }
1412 if (!getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
1413 addConfigurationKey(
17ac262c 1414 this,
e7aeea18 1415 StandardParametersKey.ConnectionTimeOut,
5edd8ba0 1416 Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
e7aeea18 1417 );
8bce55bf 1418 }
2484ac1e 1419 this.saveOcppConfiguration();
073bd098
JB
1420 }
1421
a4f7c75f
JB
1422 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1423 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
8df5ae48 1424 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
9bf0ef23 1425 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
8df5ae48 1426 }
a4f7c75f
JB
1427 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1428 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
9bf0ef23 1429 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
a4f7c75f
JB
1430 delete evseStatus.connectorsStatus;
1431 this.evses.set(evseId, {
8df5ae48 1432 ...(evseStatus as EvseStatus),
a4f7c75f 1433 connectors: new Map<number, ConnectorStatus>(
e1d9a0f4 1434 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
a4f7c75f
JB
1435 connectorId,
1436 connectorStatus,
5edd8ba0 1437 ]),
a4f7c75f
JB
1438 ),
1439 });
1440 }
1441 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1442 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1443 logger.error(`${this.logPrefix()} ${errorMsg}`);
1444 throw new BaseError(errorMsg);
1445 } else {
1446 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1447 logger.error(`${this.logPrefix()} ${errorMsg}`);
1448 throw new BaseError(errorMsg);
1449 }
1450 }
1451
34eeb1fb 1452 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
cda5d0fb 1453 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
34eeb1fb 1454 this.initializeConnectorsFromTemplate(stationTemplate);
cda5d0fb 1455 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
34eeb1fb 1456 this.initializeEvsesFromTemplate(stationTemplate);
cda5d0fb 1457 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
ae25f265
JB
1458 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1459 logger.error(`${this.logPrefix()} ${errorMsg}`);
1460 throw new BaseError(errorMsg);
1461 } else {
1462 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1463 logger.error(`${this.logPrefix()} ${errorMsg}`);
1464 throw new BaseError(errorMsg);
1465 }
1466 }
1467
34eeb1fb 1468 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1469 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
ded57f02
JB
1470 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1471 logger.error(`${this.logPrefix()} ${errorMsg}`);
1472 throw new BaseError(errorMsg);
3d25cc86 1473 }
e1d9a0f4 1474 if (!stationTemplate?.Connectors?.[0]) {
3d25cc86
JB
1475 logger.warn(
1476 `${this.logPrefix()} Charging station information from template ${
1477 this.templateFile
5edd8ba0 1478 } with no connector id 0 configuration`,
3d25cc86
JB
1479 );
1480 }
cda5d0fb
JB
1481 if (stationTemplate?.Connectors) {
1482 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
fba11dc6 1483 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
d972af76 1484 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1485 .update(
5edd8ba0 1486 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
cda5d0fb 1487 )
3d25cc86
JB
1488 .digest('hex');
1489 const connectorsConfigChanged =
1490 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1491 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1492 connectorsConfigChanged && this.connectors.clear();
1493 this.connectorsConfigurationHash = connectorsConfigHash;
269196a8
JB
1494 if (templateMaxConnectors > 0) {
1495 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1496 if (
1497 connectorId === 0 &&
1c9de2b9 1498 (!stationTemplate?.Connectors?.[connectorId] ||
cda5d0fb 1499 this.getUseConnectorId0(stationTemplate) === false)
269196a8
JB
1500 ) {
1501 continue;
1502 }
1503 const templateConnectorId =
cda5d0fb 1504 connectorId > 0 && stationTemplate?.randomConnectors
9bf0ef23 1505 ? getRandomInteger(templateMaxAvailableConnectors, 1)
269196a8 1506 : connectorId;
cda5d0fb 1507 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
fba11dc6 1508 checkStationInfoConnectorStatus(
ae25f265 1509 templateConnectorId,
04b1261c
JB
1510 connectorStatus,
1511 this.logPrefix(),
5edd8ba0 1512 this.templateFile,
04b1261c 1513 );
9bf0ef23 1514 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86 1515 }
fba11dc6 1516 initializeConnectorsMapStatus(this.connectors, this.logPrefix());
52952bf8 1517 this.saveConnectorsStatus();
ae25f265
JB
1518 } else {
1519 logger.warn(
1520 `${this.logPrefix()} Charging station information from template ${
1521 this.templateFile
5edd8ba0 1522 } with no connectors configuration defined, cannot create connectors`,
ae25f265 1523 );
3d25cc86
JB
1524 }
1525 }
1526 } else {
1527 logger.warn(
1528 `${this.logPrefix()} Charging station information from template ${
1529 this.templateFile
5edd8ba0 1530 } with no connectors configuration defined, using already defined connectors`,
3d25cc86
JB
1531 );
1532 }
3d25cc86
JB
1533 }
1534
34eeb1fb 1535 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
cda5d0fb 1536 if (!stationTemplate?.Evses && this.evses.size === 0) {
ded57f02
JB
1537 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1538 logger.error(`${this.logPrefix()} ${errorMsg}`);
1539 throw new BaseError(errorMsg);
2585c6e9 1540 }
e1d9a0f4 1541 if (!stationTemplate?.Evses?.[0]) {
2585c6e9
JB
1542 logger.warn(
1543 `${this.logPrefix()} Charging station information from template ${
1544 this.templateFile
5edd8ba0 1545 } with no evse id 0 configuration`,
2585c6e9
JB
1546 );
1547 }
e1d9a0f4 1548 if (!stationTemplate?.Evses?.[0]?.Connectors?.[0]) {
59a0f26d
JB
1549 logger.warn(
1550 `${this.logPrefix()} Charging station information from template ${
1551 this.templateFile
5edd8ba0 1552 } with evse id 0 with no connector id 0 configuration`,
59a0f26d
JB
1553 );
1554 }
491dad29
JB
1555 if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
1556 logger.warn(
1557 `${this.logPrefix()} Charging station information from template ${
1558 this.templateFile
1559 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`,
1560 );
1561 }
cda5d0fb 1562 if (stationTemplate?.Evses) {
d972af76 1563 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
ba01a213 1564 .update(JSON.stringify(stationTemplate?.Evses))
2585c6e9
JB
1565 .digest('hex');
1566 const evsesConfigChanged =
1567 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1568 if (this.evses?.size === 0 || evsesConfigChanged) {
1569 evsesConfigChanged && this.evses.clear();
1570 this.evsesConfigurationHash = evsesConfigHash;
fba11dc6 1571 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
ae25f265 1572 if (templateMaxEvses > 0) {
eb979012
JB
1573 for (const evseKey in stationTemplate.Evses) {
1574 const evseId = convertToInt(evseKey);
52952bf8 1575 this.evses.set(evseId, {
fba11dc6 1576 connectors: buildConnectorsMap(
eb979012 1577 stationTemplate?.Evses[evseKey]?.Connectors,
ae25f265 1578 this.logPrefix(),
5edd8ba0 1579 this.templateFile,
ae25f265
JB
1580 ),
1581 availability: AvailabilityType.Operative,
1582 });
e1d9a0f4 1583 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix());
ae25f265 1584 }
52952bf8 1585 this.saveEvsesStatus();
ae25f265
JB
1586 } else {
1587 logger.warn(
1588 `${this.logPrefix()} Charging station information from template ${
04b1261c 1589 this.templateFile
5edd8ba0 1590 } with no evses configuration defined, cannot create evses`,
04b1261c 1591 );
2585c6e9
JB
1592 }
1593 }
513db108
JB
1594 } else {
1595 logger.warn(
1596 `${this.logPrefix()} Charging station information from template ${
1597 this.templateFile
5edd8ba0 1598 } with no evses configuration defined, using already defined evses`,
513db108 1599 );
2585c6e9
JB
1600 }
1601 }
1602
551e477c
JB
1603 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1604 let configuration: ChargingStationConfiguration | undefined;
9bf0ef23 1605 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
073bd098 1606 try {
57adbebc
JB
1607 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1608 configuration = this.sharedLRUCache.getChargingStationConfiguration(
5edd8ba0 1609 this.configurationFileHash,
57adbebc 1610 );
7c72977b
JB
1611 } else {
1612 const measureId = `${FileType.ChargingStationConfiguration} read`;
1613 const beginId = PerformanceStatistics.beginMeasure(measureId);
1614 configuration = JSON.parse(
5edd8ba0 1615 readFileSync(this.configurationFile, 'utf8'),
7c72977b
JB
1616 ) as ChargingStationConfiguration;
1617 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1618 this.sharedLRUCache.setChargingStationConfiguration(configuration);
e1d9a0f4 1619 this.configurationFileHash = configuration.configurationHash!;
7c72977b 1620 }
073bd098 1621 } catch (error) {
fa5995d6 1622 handleFileException(
073bd098 1623 this.configurationFile,
7164966d
JB
1624 FileType.ChargingStationConfiguration,
1625 error as NodeJS.ErrnoException,
5edd8ba0 1626 this.logPrefix(),
073bd098
JB
1627 );
1628 }
1629 }
1630 return configuration;
1631 }
1632
cb60061f 1633 private saveAutomaticTransactionGeneratorConfiguration(): void {
5ced7e80
JB
1634 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1635 this.saveConfiguration();
1636 }
ac7f79af
JB
1637 }
1638
52952bf8 1639 private saveConnectorsStatus() {
7446de3b 1640 this.saveConfiguration();
52952bf8
JB
1641 }
1642
1643 private saveEvsesStatus() {
7446de3b 1644 this.saveConfiguration();
52952bf8
JB
1645 }
1646
179ed367 1647 private saveConfiguration(): void {
9bf0ef23 1648 if (isNotEmptyString(this.configurationFile)) {
2484ac1e 1649 try {
d972af76
JB
1650 if (!existsSync(dirname(this.configurationFile))) {
1651 mkdirSync(dirname(this.configurationFile), { recursive: true });
073bd098 1652 }
ae8ceef3
JB
1653 let configurationData: ChargingStationConfiguration = this.getConfigurationFromFile()
1654 ? cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1655 : {};
34eeb1fb 1656 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
52952bf8 1657 configurationData.stationInfo = this.stationInfo;
5ced7e80
JB
1658 } else {
1659 delete configurationData.stationInfo;
52952bf8 1660 }
34eeb1fb 1661 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
52952bf8 1662 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
5ced7e80
JB
1663 } else {
1664 delete configurationData.configurationKey;
52952bf8 1665 }
179ed367
JB
1666 configurationData = merge<ChargingStationConfiguration>(
1667 configurationData,
5edd8ba0 1668 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
179ed367 1669 );
5ced7e80
JB
1670 if (
1671 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1672 !this.getAutomaticTransactionGeneratorConfiguration()
1673 ) {
1674 delete configurationData.automaticTransactionGenerator;
1675 }
b1bbdae5 1676 if (this.connectors.size > 0) {
179ed367 1677 configurationData.connectorsStatus = buildConnectorsStatus(this);
5ced7e80
JB
1678 } else {
1679 delete configurationData.connectorsStatus;
52952bf8 1680 }
b1bbdae5 1681 if (this.evses.size > 0) {
179ed367 1682 configurationData.evsesStatus = buildEvsesStatus(this);
5ced7e80
JB
1683 } else {
1684 delete configurationData.evsesStatus;
52952bf8 1685 }
7c72977b 1686 delete configurationData.configurationHash;
d972af76 1687 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5ced7e80
JB
1688 .update(
1689 JSON.stringify({
1690 stationInfo: configurationData.stationInfo,
1691 configurationKey: configurationData.configurationKey,
1692 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
5edd8ba0 1693 } as ChargingStationConfiguration),
5ced7e80 1694 )
7c72977b
JB
1695 .digest('hex');
1696 if (this.configurationFileHash !== configurationHash) {
0ebf7c2e
JB
1697 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1698 configurationData.configurationHash = configurationHash;
1699 const measureId = `${FileType.ChargingStationConfiguration} write`;
1700 const beginId = PerformanceStatistics.beginMeasure(measureId);
1701 writeFileSync(
1702 this.configurationFile,
4ed03b6e 1703 JSON.stringify(configurationData, undefined, 2),
0ebf7c2e
JB
1704 'utf8',
1705 );
1706 PerformanceStatistics.endMeasure(measureId, beginId);
1707 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1708 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1709 this.configurationFileHash = configurationHash;
1710 }).catch((error) => {
1711 handleFileException(
1712 this.configurationFile,
1713 FileType.ChargingStationConfiguration,
1714 error as NodeJS.ErrnoException,
1715 this.logPrefix(),
1716 );
1717 });
7c72977b
JB
1718 } else {
1719 logger.debug(
1720 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1721 this.configurationFile
5edd8ba0 1722 }`,
7c72977b 1723 );
2484ac1e 1724 }
2484ac1e 1725 } catch (error) {
fa5995d6 1726 handleFileException(
2484ac1e 1727 this.configurationFile,
7164966d
JB
1728 FileType.ChargingStationConfiguration,
1729 error as NodeJS.ErrnoException,
5edd8ba0 1730 this.logPrefix(),
073bd098
JB
1731 );
1732 }
2484ac1e
JB
1733 } else {
1734 logger.error(
5edd8ba0 1735 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
2484ac1e 1736 );
073bd098
JB
1737 }
1738 }
1739
551e477c
JB
1740 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1741 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1742 }
1743
551e477c 1744 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
60655b26
JB
1745 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1746 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1747 return { configurationKey };
648512ce 1748 }
60655b26 1749 return undefined;
7dde0b73
JB
1750 }
1751
551e477c
JB
1752 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1753 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1754 this.getOcppConfigurationFromFile();
2484ac1e
JB
1755 if (!ocppConfiguration) {
1756 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1757 }
1758 return ocppConfiguration;
1759 }
1760
c0560973 1761 private async onOpen(): Promise<void> {
56eb297e 1762 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1 1763 logger.info(
5edd8ba0 1764 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
5144f4d1 1765 );
ed6cfcff 1766 if (this.isRegistered() === false) {
5144f4d1
JB
1767 // Send BootNotification
1768 let registrationRetryCount = 0;
1769 do {
f7f98c68 1770 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1771 BootNotificationRequest,
1772 BootNotificationResponse
8bfbc743
JB
1773 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1774 skipBufferingOnError: true,
1775 });
ed6cfcff 1776 if (this.isRegistered() === false) {
1fe0632a 1777 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
9bf0ef23 1778 await sleep(
1895299d 1779 this?.bootNotificationResponse?.interval
be4c6702 1780 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
5edd8ba0 1781 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
5144f4d1
JB
1782 );
1783 }
1784 } while (
ed6cfcff 1785 this.isRegistered() === false &&
e1d9a0f4 1786 (registrationRetryCount <= this.getRegistrationMaxRetries()! ||
5144f4d1
JB
1787 this.getRegistrationMaxRetries() === -1)
1788 );
1789 }
ed6cfcff 1790 if (this.isRegistered() === true) {
f7c2994d 1791 if (this.inAcceptedState() === true) {
94bb24d5 1792 await this.startMessageSequence();
c0560973 1793 }
5144f4d1
JB
1794 } else {
1795 logger.error(
5edd8ba0 1796 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
5144f4d1 1797 );
caad9d6b 1798 }
5144f4d1 1799 this.wsConnectionRestarted = false;
aa428a31 1800 this.autoReconnectRetryCount = 0;
c8faabc8 1801 parentPort?.postMessage(buildUpdatedMessage(this));
2e6f5966 1802 } else {
5144f4d1 1803 logger.warn(
5edd8ba0 1804 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
e7aeea18 1805 );
2e6f5966 1806 }
2e6f5966
JB
1807 }
1808
ef7d8c21 1809 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1810 switch (code) {
6c65a295
JB
1811 // Normal close
1812 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1813 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1814 logger.info(
9bf0ef23 1815 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1816 code,
1817 )}' and reason '${reason.toString()}'`,
e7aeea18 1818 );
c0560973
JB
1819 this.autoReconnectRetryCount = 0;
1820 break;
6c65a295
JB
1821 // Abnormal close
1822 default:
e7aeea18 1823 logger.error(
9bf0ef23 1824 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
5edd8ba0
JB
1825 code,
1826 )}' and reason '${reason.toString()}'`,
e7aeea18 1827 );
56eb297e 1828 this.started === true && (await this.reconnect());
c0560973
JB
1829 break;
1830 }
c8faabc8 1831 parentPort?.postMessage(buildUpdatedMessage(this));
2e6f5966
JB
1832 }
1833
56d09fd7
JB
1834 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1835 const cachedRequest = this.requests.get(messageId);
1836 if (Array.isArray(cachedRequest) === true) {
1837 return cachedRequest;
1838 }
1839 throw new OCPPError(
1840 ErrorType.PROTOCOL_ERROR,
1841 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
5edd8ba0 1842 messageType,
56d09fd7
JB
1843 )} is not an array`,
1844 undefined,
5edd8ba0 1845 cachedRequest as JsonType,
56d09fd7
JB
1846 );
1847 }
1848
1849 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1850 const [messageType, messageId, commandName, commandPayload] = request;
1851 if (this.getEnableStatistics() === true) {
1852 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1853 }
1854 logger.debug(
1855 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
5edd8ba0
JB
1856 request,
1857 )}`,
56d09fd7
JB
1858 );
1859 // Process the message
1860 await this.ocppIncomingRequestService.incomingRequestHandler(
1861 this,
1862 messageId,
1863 commandName,
5edd8ba0 1864 commandPayload,
56d09fd7
JB
1865 );
1866 }
1867
1868 private handleResponseMessage(response: Response): void {
1869 const [messageType, messageId, commandPayload] = response;
1870 if (this.requests.has(messageId) === false) {
1871 // Error
1872 throw new OCPPError(
1873 ErrorType.INTERNAL_ERROR,
1874 `Response for unknown message id ${messageId}`,
1875 undefined,
5edd8ba0 1876 commandPayload,
56d09fd7
JB
1877 );
1878 }
1879 // Respond
1880 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1881 messageType,
5edd8ba0 1882 messageId,
e1d9a0f4 1883 )!;
56d09fd7
JB
1884 logger.debug(
1885 `${this.logPrefix()} << Command '${
1886 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1887 }' received response payload: ${JSON.stringify(response)}`,
56d09fd7
JB
1888 );
1889 responseCallback(commandPayload, requestPayload);
1890 }
1891
1892 private handleErrorMessage(errorResponse: ErrorResponse): void {
1893 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1894 if (this.requests.has(messageId) === false) {
1895 // Error
1896 throw new OCPPError(
1897 ErrorType.INTERNAL_ERROR,
1898 `Error response for unknown message id ${messageId}`,
1899 undefined,
5edd8ba0 1900 { errorType, errorMessage, errorDetails },
56d09fd7
JB
1901 );
1902 }
e1d9a0f4 1903 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
56d09fd7
JB
1904 logger.debug(
1905 `${this.logPrefix()} << Command '${
1906 requestCommandName ?? Constants.UNKNOWN_COMMAND
5edd8ba0 1907 }' received error response payload: ${JSON.stringify(errorResponse)}`,
56d09fd7
JB
1908 );
1909 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1910 }
1911
ef7d8c21 1912 private async onMessage(data: RawData): Promise<void> {
e1d9a0f4
JB
1913 let request: IncomingRequest | Response | ErrorResponse | undefined;
1914 let messageType: number | undefined;
ded57f02 1915 let errorMsg: string;
c0560973 1916 try {
e1d9a0f4 1917 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 1918 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1919 if (Array.isArray(request) === true) {
56d09fd7 1920 [messageType] = request;
b3ec7bc1
JB
1921 // Check the type of message
1922 switch (messageType) {
1923 // Incoming Message
1924 case MessageType.CALL_MESSAGE:
56d09fd7 1925 await this.handleIncomingMessage(request as IncomingRequest);
b3ec7bc1 1926 break;
56d09fd7 1927 // Response Message
b3ec7bc1 1928 case MessageType.CALL_RESULT_MESSAGE:
56d09fd7 1929 this.handleResponseMessage(request as Response);
a2d1c0f1
JB
1930 break;
1931 // Error Message
1932 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1933 this.handleErrorMessage(request as ErrorResponse);
b3ec7bc1 1934 break;
56d09fd7 1935 // Unknown Message
b3ec7bc1
JB
1936 default:
1937 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ded57f02
JB
1938 errorMsg = `Wrong message type ${messageType}`;
1939 logger.error(`${this.logPrefix()} ${errorMsg}`);
1940 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
b3ec7bc1 1941 }
c8faabc8 1942 parentPort?.postMessage(buildUpdatedMessage(this));
47e22477 1943 } else {
e1d9a0f4
JB
1944 throw new OCPPError(
1945 ErrorType.PROTOCOL_ERROR,
1946 'Incoming message is not an array',
1947 undefined,
1948 {
1949 request,
1950 },
1951 );
47e22477 1952 }
c0560973 1953 } catch (error) {
e1d9a0f4
JB
1954 let commandName: IncomingRequestCommand | undefined;
1955 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined;
56d09fd7 1956 let errorCallback: ErrorCallback;
e1d9a0f4 1957 const [, messageId] = request!;
13701f69
JB
1958 switch (messageType) {
1959 case MessageType.CALL_MESSAGE:
56d09fd7 1960 [, , commandName] = request as IncomingRequest;
13701f69 1961 // Send error
56d09fd7 1962 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
13701f69
JB
1963 break;
1964 case MessageType.CALL_RESULT_MESSAGE:
1965 case MessageType.CALL_ERROR_MESSAGE:
56d09fd7 1966 if (this.requests.has(messageId) === true) {
e1d9a0f4 1967 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!;
13701f69
JB
1968 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1969 errorCallback(error as OCPPError, false);
1970 } else {
1971 // Remove the request from the cache in case of error at response handling
1972 this.requests.delete(messageId);
1973 }
de4cb8b6 1974 break;
ba7965c4 1975 }
56d09fd7
JB
1976 if (error instanceof OCPPError === false) {
1977 logger.warn(
1978 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1979 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 1980 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 1981 }' message '${data.toString()}' handling is not an OCPPError:`,
5edd8ba0 1982 error,
56d09fd7
JB
1983 );
1984 }
1985 logger.error(
1986 `${this.logPrefix()} Incoming OCPP command '${
1987 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 1988 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7
JB
1989 }' message '${data.toString()}'${
1990 messageType !== MessageType.CALL_MESSAGE
1991 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1992 : ''
1993 } processing error:`,
5edd8ba0 1994 error,
56d09fd7 1995 );
c0560973 1996 }
2328be1e
JB
1997 }
1998
c0560973 1999 private onPing(): void {
44eb6026 2000 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
2001 }
2002
2003 private onPong(): void {
44eb6026 2004 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
2005 }
2006
9534e74e 2007 private onError(error: WSError): void {
bcc9c3c0 2008 this.closeWSConnection();
44eb6026 2009 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
2010 }
2011
18bf8274 2012 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 2013 if (this.getMeteringPerTransaction() === true) {
07989fad 2014 return (
18bf8274 2015 (rounded === true
e1d9a0f4 2016 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
07989fad
JB
2017 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2018 );
2019 }
2020 return (
18bf8274 2021 (rounded === true
e1d9a0f4 2022 ? Math.round(connectorStatus.energyActiveImportRegisterValue!)
07989fad
JB
2023 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2024 );
2025 }
2026
cda5d0fb
JB
2027 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
2028 return stationTemplate?.useConnectorId0 ?? true;
8bce55bf
JB
2029 }
2030
60ddad53 2031 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
28e78158 2032 if (this.hasEvses) {
3fa7f799
JB
2033 for (const [evseId, evseStatus] of this.evses) {
2034 if (evseId === 0) {
2035 continue;
2036 }
28e78158
JB
2037 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2038 if (connectorStatus.transactionStarted === true) {
2039 await this.stopTransactionOnConnector(connectorId, reason);
2040 }
2041 }
2042 }
2043 } else {
2044 for (const connectorId of this.connectors.keys()) {
2045 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2046 await this.stopTransactionOnConnector(connectorId, reason);
2047 }
60ddad53
JB
2048 }
2049 }
2050 }
2051
1f761b9a 2052 // 0 for disabling
c72f6634 2053 private getConnectionTimeout(): number {
f2d5e3d9 2054 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)) {
e7aeea18 2055 return (
f2d5e3d9
JB
2056 parseInt(getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value!) ??
2057 Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 2058 );
291cb255 2059 }
291cb255 2060 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
2061 }
2062
1f761b9a 2063 // -1 for unlimited, 0 for disabling
72092cfc 2064 private getAutoReconnectMaxRetries(): number | undefined {
29b34879 2065 return this.stationInfo.autoReconnectMaxRetries ?? -1;
3574dfd3
JB
2066 }
2067
b89fb74f 2068 // -1 for unlimited, 0 for disabling
72092cfc 2069 private getRegistrationMaxRetries(): number | undefined {
b1bbdae5 2070 return this.stationInfo.registrationMaxRetries ?? -1;
32a1eb7a
JB
2071 }
2072
c0560973 2073 private getPowerDivider(): number {
b1bbdae5 2074 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
fa7bccf4 2075 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 2076 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
2077 }
2078 return powerDivider;
2079 }
2080
fa7bccf4
JB
2081 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2082 const maximumPower = this.getMaximumPower(stationInfo);
2083 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2084 case CurrentType.AC:
2085 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2086 this.getNumberOfPhases(stationInfo),
b1bbdae5 2087 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
5edd8ba0 2088 this.getVoltageOut(stationInfo),
cc6e8ab5
JB
2089 );
2090 case CurrentType.DC:
fa7bccf4 2091 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
2092 }
2093 }
2094
cc6e8ab5
JB
2095 private getAmperageLimitation(): number | undefined {
2096 if (
9bf0ef23 2097 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
f2d5e3d9 2098 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)
cc6e8ab5
JB
2099 ) {
2100 return (
9bf0ef23 2101 convertToInt(
f2d5e3d9 2102 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value,
fba11dc6 2103 ) / getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
2104 );
2105 }
2106 }
2107
c0560973 2108 private async startMessageSequence(): Promise<void> {
b7f9e41d 2109 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2110 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
2111 BootNotificationRequest,
2112 BootNotificationResponse
8bfbc743
JB
2113 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2114 skipBufferingOnError: true,
2115 });
6114e6f1 2116 }
136c90ba 2117 // Start WebSocket ping
c0560973 2118 this.startWebSocketPing();
5ad8570f 2119 // Start heartbeat
c0560973 2120 this.startHeartbeat();
0a60c33c 2121 // Initialize connectors status
c3b83130
JB
2122 if (this.hasEvses) {
2123 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2124 if (evseId > 0) {
2125 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
fba11dc6 2126 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
4334db72
JB
2127 await OCPPServiceUtils.sendAndSetConnectorStatus(
2128 this,
2129 connectorId,
12f26d4a 2130 connectorBootStatus,
5edd8ba0 2131 evseId,
4334db72
JB
2132 );
2133 }
c3b83130 2134 }
4334db72
JB
2135 }
2136 } else {
2137 for (const connectorId of this.connectors.keys()) {
2138 if (connectorId > 0) {
fba11dc6 2139 const connectorBootStatus = getBootConnectorStatus(
c3b83130
JB
2140 this,
2141 connectorId,
e1d9a0f4 2142 this.getConnectorStatus(connectorId)!,
c3b83130
JB
2143 );
2144 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2145 }
2146 }
5ad8570f 2147 }
c9a4f9ea
JB
2148 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2149 await this.ocppRequestService.requestHandler<
2150 FirmwareStatusNotificationRequest,
2151 FirmwareStatusNotificationResponse
2152 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2153 status: FirmwareStatus.Installed,
2154 });
2155 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 2156 }
3637ca2c 2157
0a60c33c 2158 // Start the ATG
ac7f79af 2159 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
4f69be04 2160 this.startAutomaticTransactionGenerator();
fa7bccf4 2161 }
aa428a31 2162 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
2163 }
2164
e7aeea18 2165 private async stopMessageSequence(
5edd8ba0 2166 reason: StopTransactionReason = StopTransactionReason.NONE,
e7aeea18 2167 ): Promise<void> {
136c90ba 2168 // Stop WebSocket ping
c0560973 2169 this.stopWebSocketPing();
79411696 2170 // Stop heartbeat
c0560973 2171 this.stopHeartbeat();
fa7bccf4 2172 // Stop ongoing transactions
b20eb107 2173 if (this.automaticTransactionGenerator?.started === true) {
49563992 2174 await this.stopAutomaticTransactionGenerator();
60ddad53
JB
2175 } else {
2176 await this.stopRunningTransactions(reason);
79411696 2177 }
039211f9
JB
2178 if (this.hasEvses) {
2179 for (const [evseId, evseStatus] of this.evses) {
2180 if (evseId > 0) {
2181 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2182 await this.ocppRequestService.requestHandler<
2183 StatusNotificationRequest,
2184 StatusNotificationResponse
2185 >(
2186 this,
2187 RequestCommand.STATUS_NOTIFICATION,
2188 OCPPServiceUtils.buildStatusNotificationRequest(
2189 this,
2190 connectorId,
12f26d4a 2191 ConnectorStatusEnum.Unavailable,
5edd8ba0
JB
2192 evseId,
2193 ),
039211f9
JB
2194 );
2195 delete connectorStatus?.status;
2196 }
2197 }
2198 }
2199 } else {
2200 for (const connectorId of this.connectors.keys()) {
2201 if (connectorId > 0) {
2202 await this.ocppRequestService.requestHandler<
2203 StatusNotificationRequest,
2204 StatusNotificationResponse
2205 >(
6e939d9e 2206 this,
039211f9
JB
2207 RequestCommand.STATUS_NOTIFICATION,
2208 OCPPServiceUtils.buildStatusNotificationRequest(
2209 this,
2210 connectorId,
5edd8ba0
JB
2211 ConnectorStatusEnum.Unavailable,
2212 ),
039211f9
JB
2213 );
2214 delete this.getConnectorStatus(connectorId)?.status;
2215 }
45c0ae82
JB
2216 }
2217 }
79411696
JB
2218 }
2219
c0560973 2220 private startWebSocketPing(): void {
f2d5e3d9 2221 const webSocketPingInterval: number = getConfigurationKey(
17ac262c 2222 this,
5edd8ba0 2223 StandardParametersKey.WebSocketPingInterval,
e7aeea18 2224 )
f2d5e3d9 2225 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
9cd3dfb0 2226 : 0;
ad2f27c3
JB
2227 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2228 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 2229 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2230 this.wsConnection?.ping();
136c90ba 2231 }
be4c6702 2232 }, secondsToMilliseconds(webSocketPingInterval));
e7aeea18 2233 logger.info(
9bf0ef23 2234 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
5edd8ba0
JB
2235 webSocketPingInterval,
2236 )}`,
e7aeea18 2237 );
ad2f27c3 2238 } else if (this.webSocketPingSetInterval) {
e7aeea18 2239 logger.info(
9bf0ef23 2240 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
5edd8ba0
JB
2241 webSocketPingInterval,
2242 )}`,
e7aeea18 2243 );
136c90ba 2244 } else {
e7aeea18 2245 logger.error(
5edd8ba0 2246 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
e7aeea18 2247 );
136c90ba
JB
2248 }
2249 }
2250
c0560973 2251 private stopWebSocketPing(): void {
ad2f27c3
JB
2252 if (this.webSocketPingSetInterval) {
2253 clearInterval(this.webSocketPingSetInterval);
dfe81c8f 2254 delete this.webSocketPingSetInterval;
136c90ba
JB
2255 }
2256 }
2257
1f5df42a 2258 private getConfiguredSupervisionUrl(): URL {
d5c3df49 2259 let configuredSupervisionUrl: string;
72092cfc 2260 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
9bf0ef23 2261 if (isNotEmptyArray(supervisionUrls)) {
269de583 2262 let configuredSupervisionUrlIndex: number;
2dcfe98e 2263 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2264 case SupervisionUrlDistribution.RANDOM:
e1d9a0f4
JB
2265 configuredSupervisionUrlIndex = Math.floor(
2266 secureRandom() * (supervisionUrls as string[]).length,
2267 );
2dcfe98e 2268 break;
a52a6446 2269 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2270 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2271 default:
a52a6446 2272 Object.values(SupervisionUrlDistribution).includes(
e1d9a0f4 2273 Configuration.getSupervisionUrlDistribution()!,
a52a6446
JB
2274 ) === false &&
2275 logger.error(
e1d9a0f4 2276 // eslint-disable-next-line @typescript-eslint/no-base-to-string
a52a6446
JB
2277 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2278 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
5edd8ba0 2279 }`,
a52a6446 2280 );
e1d9a0f4 2281 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length;
2dcfe98e 2282 break;
c0560973 2283 }
e1d9a0f4 2284 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex];
d5c3df49
JB
2285 } else {
2286 configuredSupervisionUrl = supervisionUrls as string;
2287 }
9bf0ef23 2288 if (isNotEmptyString(configuredSupervisionUrl)) {
d5c3df49 2289 return new URL(configuredSupervisionUrl);
c0560973 2290 }
49c508b0 2291 const errorMsg = 'No supervision url(s) configured';
7f77d16f
JB
2292 logger.error(`${this.logPrefix()} ${errorMsg}`);
2293 throw new BaseError(`${errorMsg}`);
136c90ba
JB
2294 }
2295
c0560973 2296 private stopHeartbeat(): void {
ad2f27c3
JB
2297 if (this.heartbeatSetInterval) {
2298 clearInterval(this.heartbeatSetInterval);
dfe81c8f 2299 delete this.heartbeatSetInterval;
7dde0b73 2300 }
5ad8570f
JB
2301 }
2302
55516218 2303 private terminateWSConnection(): void {
56eb297e 2304 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 2305 this.wsConnection?.terminate();
55516218
JB
2306 this.wsConnection = null;
2307 }
2308 }
2309
c72f6634 2310 private getReconnectExponentialDelay(): boolean {
a14885a3 2311 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2312 }
2313
aa428a31 2314 private async reconnect(): Promise<void> {
7874b0b1
JB
2315 // Stop WebSocket ping
2316 this.stopWebSocketPing();
136c90ba 2317 // Stop heartbeat
c0560973 2318 this.stopHeartbeat();
5ad8570f 2319 // Stop the ATG if needed
ac7f79af 2320 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
49563992 2321 await this.stopAutomaticTransactionGenerator();
ad2f27c3 2322 }
e7aeea18 2323 if (
e1d9a0f4 2324 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||
e7aeea18
JB
2325 this.getAutoReconnectMaxRetries() === -1
2326 ) {
1fe0632a 2327 ++this.autoReconnectRetryCount;
e7aeea18 2328 const reconnectDelay = this.getReconnectExponentialDelay()
9bf0ef23 2329 ? exponentialDelay(this.autoReconnectRetryCount)
be4c6702 2330 : secondsToMilliseconds(this.getConnectionTimeout());
1e080116
JB
2331 const reconnectDelayWithdraw = 1000;
2332 const reconnectTimeout =
2333 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2334 ? reconnectDelay - reconnectDelayWithdraw
2335 : 0;
e7aeea18 2336 logger.error(
9bf0ef23 2337 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
e7aeea18 2338 reconnectDelay,
5edd8ba0
JB
2339 2,
2340 )}ms, timeout ${reconnectTimeout}ms`,
e7aeea18 2341 );
9bf0ef23 2342 await sleep(reconnectDelay);
e7aeea18 2343 logger.error(
5edd8ba0 2344 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
e7aeea18
JB
2345 );
2346 this.openWSConnection(
59b6ed8d 2347 {
59b6ed8d
JB
2348 handshakeTimeout: reconnectTimeout,
2349 },
5edd8ba0 2350 { closeOpened: true },
e7aeea18 2351 );
265e4266 2352 this.wsConnectionRestarted = true;
c0560973 2353 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2354 logger.error(
d56ea27c 2355 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2356 this.autoReconnectRetryCount
5edd8ba0 2357 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,
e7aeea18 2358 );
5ad8570f
JB
2359 }
2360 }
7dde0b73 2361}