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