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