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