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