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