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