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