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