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