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