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