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