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