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