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