Apply dependencies update
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
b4d34251 2
8114d10e
JB
3import crypto from 'crypto';
4import fs from 'fs';
5import path from 'path';
ee5f26a2 6import { URL } from 'url';
8114d10e
JB
7import { parentPort } from 'worker_threads';
8
5d280aae 9import merge from 'just-merge';
ef7d8c21 10import WebSocket, { type RawData } from 'ws';
8114d10e
JB
11
12import BaseError from '../exception/BaseError';
13import OCPPError from '../exception/OCPPError';
14import PerformanceStatistics from '../performance/PerformanceStatistics';
6c1761d4 15import type { AutomaticTransactionGeneratorConfiguration } from '../types/AutomaticTransactionGenerator';
981ebfbe
JB
16import type { ChargingStationConfiguration } from '../types/ChargingStationConfiguration';
17import type { ChargingStationInfo } from '../types/ChargingStationInfo';
83e00df1 18import type { ChargingStationOcppConfiguration } from '../types/ChargingStationOcppConfiguration';
981ebfbe
JB
19import {
20 type ChargingStationTemplate,
8114d10e 21 CurrentType,
5d280aae 22 type FirmwareUpgrade,
8114d10e 23 PowerUnits,
981ebfbe 24 type WsOptions,
8114d10e 25} from '../types/ChargingStationTemplate';
8114d10e 26import { SupervisionUrlDistribution } from '../types/ConfigurationData';
6c1761d4 27import type { ConnectorStatus } from '../types/ConnectorStatus';
8114d10e 28import { FileType } from '../types/FileType';
6c1761d4 29import type { JsonType } from '../types/JsonType';
8114d10e
JB
30import { ChargingProfile, ChargingRateUnitType } from '../types/ocpp/ChargingProfile';
31import {
32 ConnectorPhaseRotation,
33 StandardParametersKey,
34 SupportedFeatureProfiles,
35 VendorDefaultParametersKey,
36} from '../types/ocpp/Configuration';
6e939d9e 37import { ConnectorStatusEnum } from '../types/ocpp/ConnectorStatusEnum';
8114d10e
JB
38import { ErrorType } from '../types/ocpp/ErrorType';
39import { MessageType } from '../types/ocpp/MessageType';
40import { MeterValue, MeterValueMeasurand } from '../types/ocpp/MeterValues';
41import { OCPPVersion } from '../types/ocpp/OCPPVersion';
e7aeea18
JB
42import {
43 AvailabilityType,
e0b0ee21
JB
44 type BootNotificationRequest,
45 type CachedRequest,
46 type ErrorCallback,
c9a4f9ea
JB
47 FirmwareStatus,
48 type FirmwareStatusNotificationRequest,
e0b0ee21
JB
49 type HeartbeatRequest,
50 type IncomingRequest,
e7aeea18 51 IncomingRequestCommand,
e0b0ee21 52 type MeterValuesRequest,
e7aeea18 53 RequestCommand,
e0b0ee21
JB
54 type ResponseCallback,
55 type StatusNotificationRequest,
e7aeea18 56} from '../types/ocpp/Requests';
f22266fd 57import {
e0b0ee21
JB
58 type BootNotificationResponse,
59 type ErrorResponse,
c9a4f9ea 60 type FirmwareStatusNotificationResponse,
e0b0ee21
JB
61 type HeartbeatResponse,
62 type MeterValuesResponse,
d270cc87 63 RegistrationStatusEnumType,
e0b0ee21
JB
64 type Response,
65 type StatusNotificationResponse,
f22266fd 66} from '../types/ocpp/Responses';
ef6fa3fb
JB
67import {
68 StopTransactionReason,
e0b0ee21
JB
69 type StopTransactionRequest,
70 type StopTransactionResponse,
ef6fa3fb 71} from '../types/ocpp/Transaction';
16b0d4e7 72import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket';
8114d10e
JB
73import Configuration from '../utils/Configuration';
74import Constants from '../utils/Constants';
75import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils';
76import FileUtils from '../utils/FileUtils';
77import logger from '../utils/Logger';
78import Utils from '../utils/Utils';
9d7484a4 79import AuthorizedTagsCache from './AuthorizedTagsCache';
6af9012e 80import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
17ac262c 81import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
17ac262c 82import { ChargingStationUtils } from './ChargingStationUtils';
89b7a234 83import ChargingStationWorkerBroadcastChannel from './ChargingStationWorkerBroadcastChannel';
32de5a57 84import { MessageChannelUtils } from './MessageChannelUtils';
e7171280 85import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService';
c0560973
JB
86import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService';
87import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService';
68c993d5 88import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils';
edd13439
JB
89import OCPP20IncomingRequestService from './ocpp/2.0/OCPP20IncomingRequestService';
90import OCPP20RequestService from './ocpp/2.0/OCPP20RequestService';
91import OCPP20ResponseService from './ocpp/2.0/OCPP20ResponseService';
6c1761d4
JB
92import type OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService';
93import type OCPPRequestService from './ocpp/OCPPRequestService';
6e939d9e 94import { OCPPServiceUtils } from './ocpp/OCPPServiceUtils';
57adbebc 95import SharedLRUCache from './SharedLRUCache';
3f40bc9c
JB
96
97export default class ChargingStation {
c72f6634 98 public readonly index: number;
2484ac1e 99 public readonly templateFile: string;
6e0964c8 100 public stationInfo!: ChargingStationInfo;
452a82ca 101 public started: boolean;
cbf9b878 102 public starting: boolean;
a5e9befc
JB
103 public authorizedTagsCache: AuthorizedTagsCache;
104 public automaticTransactionGenerator!: AutomaticTransactionGenerator;
2484ac1e 105 public ocppConfiguration!: ChargingStationOcppConfiguration;
6e0964c8 106 public wsConnection!: WebSocket;
a5e9befc 107 public readonly connectors: Map<number, ConnectorStatus>;
9e23580d 108 public readonly requests: Map<string, CachedRequest>;
6e0964c8
JB
109 public performanceStatistics!: PerformanceStatistics;
110 public heartbeatSetInterval!: NodeJS.Timeout;
6e0964c8 111 public ocppRequestService!: OCPPRequestService;
0a03f36c 112 public bootNotificationRequest!: BootNotificationRequest;
ae711c83 113 public bootNotificationResponse!: BootNotificationResponse | null;
fa7bccf4 114 public powerDivider!: number;
950b1349 115 private stopping: boolean;
073bd098 116 private configurationFile!: string;
7c72977b 117 private configurationFileHash!: string;
6e0964c8 118 private connectorsConfigurationHash!: string;
a472cf2b 119 private ocppIncomingRequestService!: OCPPIncomingRequestService;
8e242273 120 private readonly messageBuffer: Set<string>;
fa7bccf4 121 private configuredSupervisionUrl!: URL;
c72f6634 122 private configuredSupervisionUrlIndex!: number;
265e4266 123 private wsConnectionRestarted: boolean;
ad2f27c3 124 private autoReconnectRetryCount: number;
9d7484a4 125 private templateFileWatcher!: fs.FSWatcher;
57adbebc 126 private readonly sharedLRUCache: SharedLRUCache;
6e0964c8 127 private webSocketPingSetInterval!: NodeJS.Timeout;
89b7a234 128 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
6af9012e 129
2484ac1e 130 constructor(index: number, templateFile: string) {
aa428a31 131 this.started = false;
950b1349
JB
132 this.starting = false;
133 this.stopping = false;
aa428a31
JB
134 this.wsConnectionRestarted = false;
135 this.autoReconnectRetryCount = 0;
ad2f27c3 136 this.index = index;
2484ac1e 137 this.templateFile = templateFile;
9f2e3130 138 this.connectors = new Map<number, ConnectorStatus>();
32b02249 139 this.requests = new Map<string, CachedRequest>();
8e242273 140 this.messageBuffer = new Set<string>();
b44b779a
JB
141 this.sharedLRUCache = SharedLRUCache.getInstance();
142 this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
89b7a234 143 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
32de5a57 144
9f2e3130 145 this.initialize();
c0560973
JB
146 }
147
25f5a959 148 private get wsConnectionUrl(): URL {
fa7bccf4
JB
149 return new URL(
150 (this.getSupervisionUrlOcppConfiguration()
151 ? ChargingStationConfigurationUtils.getConfigurationKey(
17ac262c
JB
152 this,
153 this.getSupervisionUrlOcppKey()
fa7bccf4
JB
154 ).value
155 : this.configuredSupervisionUrl.href) +
156 '/' +
157 this.stationInfo.chargingStationId
158 );
12fc74d6
JB
159 }
160
c0560973 161 public logPrefix(): string {
ccb1d6e9
JB
162 return Utils.logPrefix(
163 ` ${
164 this?.stationInfo?.chargingStationId ??
165 ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile())
166 } |`
167 );
c0560973
JB
168 }
169
c0560973 170 public hasAuthorizedTags(): boolean {
9d7484a4
JB
171 return !Utils.isEmptyArray(
172 this.authorizedTagsCache.getAuthorizedTags(
173 ChargingStationUtils.getAuthorizationFile(this.stationInfo)
174 )
175 );
c0560973
JB
176 }
177
ad774cec
JB
178 public getEnableStatistics(): boolean {
179 return this.stationInfo.enableStatistics ?? false;
c0560973
JB
180 }
181
ad774cec 182 public getMustAuthorizeAtRemoteStart(): boolean {
03ebf4c1 183 return this.stationInfo.mustAuthorizeAtRemoteStart ?? true;
a7fc8211
JB
184 }
185
ad774cec 186 public getPayloadSchemaValidation(): boolean {
e3018bc4
JB
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;
ad774cec 490 if (this.getEnableStatistics() === true) {
0d8852a5
JB
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 }
ad774cec 518 if (this.getEnableStatistics() === true) {
0d8852a5
JB
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 549 this.closeWSConnection();
ad774cec 550 if (this.getEnableStatistics() === true) {
0d8852a5
JB
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
5d280aae
JB
568 public getFirmwareUpgrade(): FirmwareUpgrade {
569 return merge(
570 {
571 reset: true,
572 },
573 this.stationInfo.firmwareUpgrade
574 );
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 }
edd13439 644 const ocppVersion = this.getOcppVersion();
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 );
846 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
847 if (!Utils.isEmptyArray(stationTemplate.power)) {
848 stationTemplate.power = stationTemplate.power as number[];
849 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
cc6e8ab5 850 stationInfo.maximumPower =
fa7bccf4
JB
851 stationTemplate.powerUnit === PowerUnits.KILO_WATT
852 ? stationTemplate.power[powerArrayRandomIndex] * 1000
853 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 854 } else {
fa7bccf4 855 stationTemplate.power = stationTemplate.power as number;
cc6e8ab5 856 stationInfo.maximumPower =
fa7bccf4
JB
857 stationTemplate.powerUnit === PowerUnits.KILO_WATT
858 ? stationTemplate.power * 1000
859 : stationTemplate.power;
860 }
3637ca2c
JB
861 stationInfo.firmwareVersionPattern =
862 stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
863 if (
864 stationInfo.firmwareVersion &&
865 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
866 ) {
867 logger.warn(
868 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
869 this.templateFile
870 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
871 );
872 }
fa7bccf4
JB
873 stationInfo.resetTime = stationTemplate.resetTime
874 ? stationTemplate.resetTime * 1000
e7aeea18 875 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
c72f6634
JB
876 const configuredMaxConnectors =
877 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
fa7bccf4
JB
878 ChargingStationUtils.checkConfiguredMaxConnectors(
879 configuredMaxConnectors,
880 this.templateFile,
fc040c43 881 this.logPrefix()
fa7bccf4
JB
882 );
883 const templateMaxConnectors =
884 ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
885 ChargingStationUtils.checkTemplateMaxConnectors(
886 templateMaxConnectors,
887 this.templateFile,
fc040c43 888 this.logPrefix()
fa7bccf4
JB
889 );
890 if (
891 configuredMaxConnectors >
892 (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
893 !stationTemplate?.randomConnectors
894 ) {
895 logger.warn(
896 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
897 this.templateFile
898 }, forcing random connector configurations affectation`
899 );
900 stationInfo.randomConnectors = true;
901 }
902 // Build connectors if needed (FIXME: should be factored out)
903 this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors);
904 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
905 ChargingStationUtils.createStationInfoHash(stationInfo);
9ac86a7e 906 return stationInfo;
5ad8570f
JB
907 }
908
ccb1d6e9
JB
909 private getStationInfoFromFile(): ChargingStationInfo | null {
910 let stationInfo: ChargingStationInfo = null;
fa7bccf4
JB
911 this.getStationInfoPersistentConfiguration() &&
912 (stationInfo = this.getConfigurationFromFile()?.stationInfo ?? null);
913 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
f765beaa 914 return stationInfo;
2484ac1e
JB
915 }
916
917 private getStationInfo(): ChargingStationInfo {
918 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
2484ac1e 919 const stationInfoFromFile: ChargingStationInfo = this.getStationInfoFromFile();
aca53a1a 920 // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
f765beaa 921 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
01efc60a
JB
922 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
923 return this.stationInfo;
924 }
2484ac1e 925 return stationInfoFromFile;
f765beaa 926 }
fec4d204
JB
927 stationInfoFromFile &&
928 ChargingStationUtils.propagateSerialNumber(
929 this.getTemplateFromFile(),
930 stationInfoFromFile,
931 stationInfoFromTemplate
932 );
01efc60a 933 return stationInfoFromTemplate;
2484ac1e
JB
934 }
935
936 private saveStationInfo(): void {
ccb1d6e9 937 if (this.getStationInfoPersistentConfiguration()) {
7c72977b 938 this.saveConfiguration();
ccb1d6e9 939 }
2484ac1e
JB
940 }
941
1f5df42a 942 private getOcppVersion(): OCPPVersion {
aba95196 943 return this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
c0560973
JB
944 }
945
e8e865ea 946 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
947 return this.stationInfo?.ocppPersistentConfiguration ?? true;
948 }
949
950 private getStationInfoPersistentConfiguration(): boolean {
951 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
952 }
953
c0560973 954 private handleUnsupportedVersion(version: OCPPVersion) {
fc040c43
JB
955 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
956 logger.error(`${this.logPrefix()} ${errMsg}`);
6c8f5d90 957 throw new BaseError(errMsg);
c0560973
JB
958 }
959
2484ac1e 960 private initialize(): void {
fa7bccf4 961 this.configurationFile = path.join(
ee5f26a2 962 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
b44b779a 963 ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile()) + '.json'
0642c3d2 964 );
b44b779a
JB
965 this.stationInfo = this.getStationInfo();
966 this.saveStationInfo();
7a3a2ebb 967 // Avoid duplication of connectors related information in RAM
94ec7e96 968 this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
fa7bccf4 969 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
ad774cec 970 if (this.getEnableStatistics() === true) {
0642c3d2 971 this.performanceStatistics = PerformanceStatistics.getInstance(
51c83d6f 972 this.stationInfo.hashId,
0642c3d2 973 this.stationInfo.chargingStationId,
fa7bccf4 974 this.configuredSupervisionUrl
0642c3d2
JB
975 );
976 }
fa7bccf4
JB
977 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
978 this.stationInfo
979 );
fa7bccf4
JB
980 this.powerDivider = this.getPowerDivider();
981 // OCPP configuration
982 this.ocppConfiguration = this.getOcppConfiguration();
983 this.initializeOcppConfiguration();
1f5df42a 984 switch (this.getOcppVersion()) {
c0560973 985 case OCPPVersion.VERSION_16:
e7aeea18 986 this.ocppIncomingRequestService =
08f130a0 987 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
e7aeea18 988 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
08f130a0 989 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
e7aeea18 990 );
c0560973 991 break;
edd13439
JB
992 case OCPPVersion.VERSION_20:
993 case OCPPVersion.VERSION_201:
994 this.ocppIncomingRequestService =
995 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
996 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
997 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
998 );
999 break;
c0560973 1000 default:
1f5df42a 1001 this.handleUnsupportedVersion(this.getOcppVersion());
c0560973
JB
1002 break;
1003 }
b7f9e41d 1004 if (this.stationInfo?.autoRegister === true) {
47e22477 1005 this.bootNotificationResponse = {
d270cc87 1006 currentTime: new Date(),
47e22477 1007 interval: this.getHeartbeatInterval() / 1000,
d270cc87 1008 status: RegistrationStatusEnumType.ACCEPTED,
47e22477
JB
1009 };
1010 }
3637ca2c
JB
1011 if (
1012 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1013 this.stationInfo.firmwareVersion &&
1014 this.stationInfo.firmwareVersionPattern
1015 ) {
5d280aae
JB
1016 const versionStep = this.getFirmwareUpgrade()?.versionUpgrade?.step ?? 1;
1017 const patternGroup: number =
1018 this.getFirmwareUpgrade()?.versionUpgrade?.patternGroup ??
1019 this.stationInfo.firmwareVersion.split('.').length;
3637ca2c
JB
1020 const match = this.stationInfo.firmwareVersion
1021 .match(new RegExp(this.stationInfo.firmwareVersionPattern))
5d280aae 1022 .slice(1, patternGroup + 1);
3637ca2c 1023 const patchLevelIndex = match.length - 1;
5d280aae
JB
1024 match[patchLevelIndex] = (
1025 Utils.convertToInt(match[patchLevelIndex]) + versionStep
1026 ).toString();
3637ca2c
JB
1027 this.stationInfo.firmwareVersion = match.join('.');
1028 }
147d0e0f
JB
1029 }
1030
2484ac1e 1031 private initializeOcppConfiguration(): void {
17ac262c
JB
1032 if (
1033 !ChargingStationConfigurationUtils.getConfigurationKey(
1034 this,
1035 StandardParametersKey.HeartbeatInterval
1036 )
1037 ) {
1038 ChargingStationConfigurationUtils.addConfigurationKey(
1039 this,
1040 StandardParametersKey.HeartbeatInterval,
1041 '0'
1042 );
f0f65a62 1043 }
17ac262c
JB
1044 if (
1045 !ChargingStationConfigurationUtils.getConfigurationKey(
1046 this,
1047 StandardParametersKey.HeartBeatInterval
1048 )
1049 ) {
1050 ChargingStationConfigurationUtils.addConfigurationKey(
1051 this,
1052 StandardParametersKey.HeartBeatInterval,
1053 '0',
1054 { visible: false }
1055 );
f0f65a62 1056 }
e7aeea18
JB
1057 if (
1058 this.getSupervisionUrlOcppConfiguration() &&
17ac262c 1059 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1060 ) {
17ac262c
JB
1061 ChargingStationConfigurationUtils.addConfigurationKey(
1062 this,
a59737e3 1063 this.getSupervisionUrlOcppKey(),
fa7bccf4 1064 this.configuredSupervisionUrl.href,
e7aeea18
JB
1065 { reboot: true }
1066 );
e6895390
JB
1067 } else if (
1068 !this.getSupervisionUrlOcppConfiguration() &&
17ac262c 1069 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1070 ) {
17ac262c
JB
1071 ChargingStationConfigurationUtils.deleteConfigurationKey(
1072 this,
1073 this.getSupervisionUrlOcppKey(),
1074 { save: false }
1075 );
12fc74d6 1076 }
cc6e8ab5
JB
1077 if (
1078 this.stationInfo.amperageLimitationOcppKey &&
17ac262c
JB
1079 !ChargingStationConfigurationUtils.getConfigurationKey(
1080 this,
1081 this.stationInfo.amperageLimitationOcppKey
1082 )
cc6e8ab5 1083 ) {
17ac262c
JB
1084 ChargingStationConfigurationUtils.addConfigurationKey(
1085 this,
cc6e8ab5 1086 this.stationInfo.amperageLimitationOcppKey,
17ac262c
JB
1087 (
1088 this.stationInfo.maximumAmperage *
1089 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1090 ).toString()
cc6e8ab5
JB
1091 );
1092 }
17ac262c
JB
1093 if (
1094 !ChargingStationConfigurationUtils.getConfigurationKey(
1095 this,
1096 StandardParametersKey.SupportedFeatureProfiles
1097 )
1098 ) {
1099 ChargingStationConfigurationUtils.addConfigurationKey(
1100 this,
e7aeea18 1101 StandardParametersKey.SupportedFeatureProfiles,
b22787b4 1102 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
e7aeea18
JB
1103 );
1104 }
17ac262c
JB
1105 ChargingStationConfigurationUtils.addConfigurationKey(
1106 this,
e7aeea18
JB
1107 StandardParametersKey.NumberOfConnectors,
1108 this.getNumberOfConnectors().toString(),
a95873d8
JB
1109 { readonly: true },
1110 { overwrite: true }
e7aeea18 1111 );
17ac262c
JB
1112 if (
1113 !ChargingStationConfigurationUtils.getConfigurationKey(
1114 this,
1115 StandardParametersKey.MeterValuesSampledData
1116 )
1117 ) {
1118 ChargingStationConfigurationUtils.addConfigurationKey(
1119 this,
e7aeea18
JB
1120 StandardParametersKey.MeterValuesSampledData,
1121 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1122 );
7abfea5f 1123 }
17ac262c
JB
1124 if (
1125 !ChargingStationConfigurationUtils.getConfigurationKey(
1126 this,
1127 StandardParametersKey.ConnectorPhaseRotation
1128 )
1129 ) {
7e1dc878 1130 const connectorPhaseRotation = [];
734d790d 1131 for (const connectorId of this.connectors.keys()) {
7e1dc878 1132 // AC/DC
734d790d
JB
1133 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1134 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1135 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1136 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
e7aeea18 1137 // AC
734d790d
JB
1138 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1139 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1140 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1141 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
7e1dc878
JB
1142 }
1143 }
17ac262c
JB
1144 ChargingStationConfigurationUtils.addConfigurationKey(
1145 this,
e7aeea18
JB
1146 StandardParametersKey.ConnectorPhaseRotation,
1147 connectorPhaseRotation.toString()
1148 );
7e1dc878 1149 }
e7aeea18 1150 if (
17ac262c
JB
1151 !ChargingStationConfigurationUtils.getConfigurationKey(
1152 this,
1153 StandardParametersKey.AuthorizeRemoteTxRequests
e7aeea18
JB
1154 )
1155 ) {
17ac262c
JB
1156 ChargingStationConfigurationUtils.addConfigurationKey(
1157 this,
1158 StandardParametersKey.AuthorizeRemoteTxRequests,
1159 'true'
1160 );
36f6a92e 1161 }
17ac262c
JB
1162 if (
1163 !ChargingStationConfigurationUtils.getConfigurationKey(
1164 this,
1165 StandardParametersKey.LocalAuthListEnabled
1166 ) &&
1167 ChargingStationConfigurationUtils.getConfigurationKey(
1168 this,
1169 StandardParametersKey.SupportedFeatureProfiles
1170 )?.value.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1171 ) {
1172 ChargingStationConfigurationUtils.addConfigurationKey(
1173 this,
1174 StandardParametersKey.LocalAuthListEnabled,
1175 'false'
1176 );
1177 }
1178 if (
1179 !ChargingStationConfigurationUtils.getConfigurationKey(
1180 this,
1181 StandardParametersKey.ConnectionTimeOut
1182 )
1183 ) {
1184 ChargingStationConfigurationUtils.addConfigurationKey(
1185 this,
e7aeea18
JB
1186 StandardParametersKey.ConnectionTimeOut,
1187 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1188 );
8bce55bf 1189 }
2484ac1e 1190 this.saveOcppConfiguration();
073bd098
JB
1191 }
1192
3d25cc86
JB
1193 private initializeConnectors(
1194 stationInfo: ChargingStationInfo,
fa7bccf4 1195 configuredMaxConnectors: number,
3d25cc86
JB
1196 templateMaxConnectors: number
1197 ): void {
1198 if (!stationInfo?.Connectors && this.connectors.size === 0) {
fc040c43
JB
1199 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1200 logger.error(`${this.logPrefix()} ${logMsg}`);
3d25cc86
JB
1201 throw new BaseError(logMsg);
1202 }
1203 if (!stationInfo?.Connectors[0]) {
1204 logger.warn(
1205 `${this.logPrefix()} Charging station information from template ${
1206 this.templateFile
1207 } with no connector Id 0 configuration`
1208 );
1209 }
1210 if (stationInfo?.Connectors) {
1211 const connectorsConfigHash = crypto
1212 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4 1213 .update(JSON.stringify(stationInfo?.Connectors) + configuredMaxConnectors.toString())
3d25cc86
JB
1214 .digest('hex');
1215 const connectorsConfigChanged =
1216 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1217 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1218 connectorsConfigChanged && this.connectors.clear();
1219 this.connectorsConfigurationHash = connectorsConfigHash;
1220 // Add connector Id 0
1221 let lastConnector = '0';
1222 for (lastConnector in stationInfo?.Connectors) {
56eb297e 1223 const connectorStatus = stationInfo?.Connectors[lastConnector];
3d25cc86
JB
1224 const lastConnectorId = Utils.convertToInt(lastConnector);
1225 if (
1226 lastConnectorId === 0 &&
bb83b5ed 1227 this.getUseConnectorId0(stationInfo) === true &&
56eb297e 1228 connectorStatus
3d25cc86 1229 ) {
56eb297e 1230 this.checkStationInfoConnectorStatus(lastConnectorId, connectorStatus);
3d25cc86
JB
1231 this.connectors.set(
1232 lastConnectorId,
56eb297e 1233 Utils.cloneObject<ConnectorStatus>(connectorStatus)
3d25cc86
JB
1234 );
1235 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1236 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1237 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1238 }
1239 }
1240 }
1241 // Generate all connectors
1242 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
fa7bccf4 1243 for (let index = 1; index <= configuredMaxConnectors; index++) {
ccb1d6e9 1244 const randConnectorId = stationInfo?.randomConnectors
3d25cc86
JB
1245 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1246 : index;
56eb297e
JB
1247 const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
1248 this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
1249 this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86
JB
1250 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1251 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1252 this.getConnectorStatus(index).chargingProfiles = [];
1253 }
1254 }
1255 }
1256 }
1257 } else {
1258 logger.warn(
1259 `${this.logPrefix()} Charging station information from template ${
1260 this.templateFile
1261 } with no connectors configuration defined, using already defined connectors`
1262 );
1263 }
1264 // Initialize transaction attributes on connectors
1265 for (const connectorId of this.connectors.keys()) {
f5b51071
JB
1266 if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionStarted === true) {
1267 logger.warn(
1268 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
1269 this.getConnectorStatus(connectorId).transactionId
1270 }`
1271 );
1272 }
1984f194
JB
1273 if (
1274 connectorId > 0 &&
1275 (this.getConnectorStatus(connectorId).transactionStarted === undefined ||
f5b51071 1276 this.getConnectorStatus(connectorId).transactionStarted === null)
1984f194 1277 ) {
3d25cc86
JB
1278 this.initializeConnectorStatus(connectorId);
1279 }
1280 }
1281 }
1282
56eb297e
JB
1283 private checkStationInfoConnectorStatus(
1284 connectorId: number,
1285 connectorStatus: ConnectorStatus
1286 ): void {
1287 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1288 logger.warn(
1289 `${this.logPrefix()} Charging station information from template ${
1290 this.templateFile
1291 } with connector ${connectorId} status configuration defined, undefine it`
1292 );
1293 connectorStatus.status = undefined;
1294 }
1295 }
1296
7f7b65ca 1297 private getConfigurationFromFile(): ChargingStationConfiguration | null {
073bd098 1298 let configuration: ChargingStationConfiguration = null;
2484ac1e 1299 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1300 try {
57adbebc
JB
1301 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1302 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1303 this.configurationFileHash
1304 );
7c72977b
JB
1305 } else {
1306 const measureId = `${FileType.ChargingStationConfiguration} read`;
1307 const beginId = PerformanceStatistics.beginMeasure(measureId);
1308 configuration = JSON.parse(
1309 fs.readFileSync(this.configurationFile, 'utf8')
1310 ) as ChargingStationConfiguration;
1311 PerformanceStatistics.endMeasure(measureId, beginId);
1312 this.configurationFileHash = configuration.configurationHash;
57adbebc 1313 this.sharedLRUCache.setChargingStationConfiguration(configuration);
7c72977b 1314 }
073bd098
JB
1315 } catch (error) {
1316 FileUtils.handleFileException(
1317 this.logPrefix(),
a95873d8 1318 FileType.ChargingStationConfiguration,
073bd098
JB
1319 this.configurationFile,
1320 error as NodeJS.ErrnoException
1321 );
1322 }
1323 }
1324 return configuration;
1325 }
1326
7c72977b 1327 private saveConfiguration(): void {
2484ac1e
JB
1328 if (this.configurationFile) {
1329 try {
2484ac1e
JB
1330 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1331 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1332 }
ccb1d6e9
JB
1333 const configurationData: ChargingStationConfiguration =
1334 this.getConfigurationFromFile() ?? {};
7c72977b
JB
1335 this.ocppConfiguration?.configurationKey &&
1336 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1337 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1338 delete configurationData.configurationHash;
1339 const configurationHash = crypto
1340 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1341 .update(JSON.stringify(configurationData))
1342 .digest('hex');
1343 if (this.configurationFileHash !== configurationHash) {
1344 configurationData.configurationHash = configurationHash;
1345 const measureId = `${FileType.ChargingStationConfiguration} write`;
1346 const beginId = PerformanceStatistics.beginMeasure(measureId);
1347 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1348 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1349 fs.closeSync(fileDescriptor);
1350 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1351 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1352 this.configurationFileHash = configurationHash;
57adbebc 1353 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1354 } else {
1355 logger.debug(
1356 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1357 this.configurationFile
1358 }`
1359 );
2484ac1e 1360 }
2484ac1e
JB
1361 } catch (error) {
1362 FileUtils.handleFileException(
1363 this.logPrefix(),
1364 FileType.ChargingStationConfiguration,
1365 this.configurationFile,
1366 error as NodeJS.ErrnoException
073bd098
JB
1367 );
1368 }
2484ac1e
JB
1369 } else {
1370 logger.error(
01efc60a 1371 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1372 );
073bd098
JB
1373 }
1374 }
1375
ccb1d6e9
JB
1376 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | null {
1377 return this.getTemplateFromFile()?.Configuration ?? null;
2484ac1e
JB
1378 }
1379
1380 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | null {
1381 let configuration: ChargingStationConfiguration = null;
23290150 1382 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1383 const configurationFromFile = this.getConfigurationFromFile();
1384 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1385 }
2484ac1e 1386 configuration && delete configuration.stationInfo;
073bd098 1387 return configuration;
7dde0b73
JB
1388 }
1389
ccb1d6e9 1390 private getOcppConfiguration(): ChargingStationOcppConfiguration | null {
2484ac1e
JB
1391 let ocppConfiguration: ChargingStationOcppConfiguration = this.getOcppConfigurationFromFile();
1392 if (!ocppConfiguration) {
1393 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1394 }
1395 return ocppConfiguration;
1396 }
1397
c0560973 1398 private async onOpen(): Promise<void> {
56eb297e 1399 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1400 logger.info(
1401 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1402 );
ed6cfcff 1403 if (this.isRegistered() === false) {
5144f4d1
JB
1404 // Send BootNotification
1405 let registrationRetryCount = 0;
1406 do {
f7f98c68 1407 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1408 BootNotificationRequest,
1409 BootNotificationResponse
8bfbc743
JB
1410 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1411 skipBufferingOnError: true,
1412 });
ed6cfcff 1413 if (this.isRegistered() === false) {
5144f4d1
JB
1414 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1415 await Utils.sleep(
1416 this.bootNotificationResponse?.interval
1417 ? this.bootNotificationResponse.interval * 1000
1418 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1419 );
1420 }
1421 } while (
ed6cfcff 1422 this.isRegistered() === false &&
5144f4d1
JB
1423 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1424 this.getRegistrationMaxRetries() === -1)
1425 );
1426 }
ed6cfcff 1427 if (this.isRegistered() === true) {
23290150 1428 if (this.isInAcceptedState() === true) {
94bb24d5 1429 await this.startMessageSequence();
c0560973 1430 }
5144f4d1
JB
1431 } else {
1432 logger.error(
1433 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1434 );
caad9d6b 1435 }
5144f4d1 1436 this.wsConnectionRestarted = false;
aa428a31 1437 this.autoReconnectRetryCount = 0;
5e3cb728 1438 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1439 } else {
5144f4d1
JB
1440 logger.warn(
1441 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1442 );
2e6f5966 1443 }
2e6f5966
JB
1444 }
1445
ef7d8c21 1446 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1447 switch (code) {
6c65a295
JB
1448 // Normal close
1449 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1450 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1451 logger.info(
5e3cb728 1452 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1453 code
ef7d8c21 1454 )}' and reason '${reason.toString()}'`
e7aeea18 1455 );
c0560973
JB
1456 this.autoReconnectRetryCount = 0;
1457 break;
6c65a295
JB
1458 // Abnormal close
1459 default:
e7aeea18 1460 logger.error(
5e3cb728 1461 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1462 code
ef7d8c21 1463 )}' and reason '${reason.toString()}'`
e7aeea18 1464 );
56eb297e 1465 this.started === true && (await this.reconnect());
c0560973
JB
1466 break;
1467 }
5e3cb728 1468 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1469 }
1470
ef7d8c21 1471 private async onMessage(data: RawData): Promise<void> {
b3ec7bc1
JB
1472 let messageType: number;
1473 let messageId: string;
1474 let commandName: IncomingRequestCommand;
1475 let commandPayload: JsonType;
1476 let errorType: ErrorType;
1477 let errorMessage: string;
1478 let errorDetails: JsonType;
d900c8d7
JB
1479 let responseCallback: ResponseCallback;
1480 let errorCallback: ErrorCallback;
32b02249 1481 let requestCommandName: RequestCommand | IncomingRequestCommand;
b3ec7bc1 1482 let requestPayload: JsonType;
32b02249 1483 let cachedRequest: CachedRequest;
c0560973
JB
1484 let errMsg: string;
1485 try {
b3ec7bc1 1486 const request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1487 if (Array.isArray(request) === true) {
9934652c 1488 [messageType, messageId] = request;
b3ec7bc1
JB
1489 // Check the type of message
1490 switch (messageType) {
1491 // Incoming Message
1492 case MessageType.CALL_MESSAGE:
9934652c 1493 [, , commandName, commandPayload] = request as IncomingRequest;
0638ddd2 1494 if (this.getEnableStatistics() === true) {
b3ec7bc1
JB
1495 this.performanceStatistics.addRequestStatistic(commandName, messageType);
1496 }
1497 logger.debug(
1498 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1499 request
1500 )}`
1501 );
1502 // Process the message
1503 await this.ocppIncomingRequestService.incomingRequestHandler(
08f130a0 1504 this,
b3ec7bc1
JB
1505 messageId,
1506 commandName,
1507 commandPayload
1508 );
1509 break;
1510 // Outcome Message
1511 case MessageType.CALL_RESULT_MESSAGE:
9934652c 1512 [, , commandPayload] = request as Response;
ba7965c4 1513 if (this.requests.has(messageId) === false) {
a2d1c0f1
JB
1514 // Error
1515 throw new OCPPError(
1516 ErrorType.INTERNAL_ERROR,
1517 `Response for unknown message id ${messageId}`,
1518 null,
1519 commandPayload
1520 );
1521 }
b3ec7bc1
JB
1522 // Respond
1523 cachedRequest = this.requests.get(messageId);
53e5fd67 1524 if (Array.isArray(cachedRequest) === true) {
53e07f94 1525 [responseCallback, errorCallback, requestCommandName, requestPayload] = cachedRequest;
b3ec7bc1
JB
1526 } else {
1527 throw new OCPPError(
1528 ErrorType.PROTOCOL_ERROR,
53e5fd67 1529 `Cached request for message id ${messageId} response is not an array`,
c2bc716f
JB
1530 null,
1531 cachedRequest as unknown as JsonType
b3ec7bc1
JB
1532 );
1533 }
1534 logger.debug(
7ec6c5c9 1535 `${this.logPrefix()} << Command '${
2a232a18 1536 requestCommandName ?? Constants.UNKNOWN_COMMAND
7ec6c5c9 1537 }' received response payload: ${JSON.stringify(request)}`
b3ec7bc1 1538 );
a2d1c0f1
JB
1539 responseCallback(commandPayload, requestPayload);
1540 break;
1541 // Error Message
1542 case MessageType.CALL_ERROR_MESSAGE:
1543 [, , errorType, errorMessage, errorDetails] = request as ErrorResponse;
ba7965c4 1544 if (this.requests.has(messageId) === false) {
b3ec7bc1
JB
1545 // Error
1546 throw new OCPPError(
1547 ErrorType.INTERNAL_ERROR,
a2d1c0f1 1548 `Error response for unknown message id ${messageId}`,
c2bc716f 1549 null,
a2d1c0f1 1550 { errorType, errorMessage, errorDetails }
b3ec7bc1
JB
1551 );
1552 }
b3ec7bc1 1553 cachedRequest = this.requests.get(messageId);
53e5fd67 1554 if (Array.isArray(cachedRequest) === true) {
a2d1c0f1 1555 [, errorCallback, requestCommandName] = cachedRequest;
b3ec7bc1
JB
1556 } else {
1557 throw new OCPPError(
1558 ErrorType.PROTOCOL_ERROR,
53e5fd67 1559 `Cached request for message id ${messageId} error response is not an array`,
c2bc716f
JB
1560 null,
1561 cachedRequest as unknown as JsonType
b3ec7bc1
JB
1562 );
1563 }
1564 logger.debug(
7ec6c5c9 1565 `${this.logPrefix()} << Command '${
2a232a18 1566 requestCommandName ?? Constants.UNKNOWN_COMMAND
7ec6c5c9 1567 }' received error payload: ${JSON.stringify(request)}`
b3ec7bc1 1568 );
a2d1c0f1 1569 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
b3ec7bc1
JB
1570 break;
1571 // Error
1572 default:
1573 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fc040c43
JB
1574 errMsg = `Wrong message type ${messageType}`;
1575 logger.error(`${this.logPrefix()} ${errMsg}`);
b3ec7bc1
JB
1576 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1577 }
32de5a57 1578 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1579 } else {
53e5fd67 1580 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1581 request,
ac54a9bb 1582 });
47e22477 1583 }
c0560973
JB
1584 } catch (error) {
1585 // Log
e7aeea18 1586 logger.error(
91a4f151 1587 `${this.logPrefix()} Incoming OCPP command '${
2a232a18 1588 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
9c5d9fa4
JB
1589 }' message '${data.toString()}'${
1590 messageType !== MessageType.CALL_MESSAGE
1591 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1592 : ''
1593 } processing error:`,
e7aeea18
JB
1594 error
1595 );
ba7965c4 1596 if (error instanceof OCPPError === false) {
247659af 1597 logger.warn(
91a4f151 1598 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2a232a18 1599 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
fc040c43 1600 }' message '${data.toString()}' handling is not an OCPPError:`,
247659af
JB
1601 error
1602 );
1603 }
13701f69
JB
1604 switch (messageType) {
1605 case MessageType.CALL_MESSAGE:
1606 // Send error
1607 await this.ocppRequestService.sendError(
1608 this,
1609 messageId,
1610 error as OCPPError,
1611 commandName ?? requestCommandName ?? null
1612 );
1613 break;
1614 case MessageType.CALL_RESULT_MESSAGE:
1615 case MessageType.CALL_ERROR_MESSAGE:
1616 if (errorCallback) {
1617 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1618 errorCallback(error as OCPPError, false);
1619 } else {
1620 // Remove the request from the cache in case of error at response handling
1621 this.requests.delete(messageId);
1622 }
de4cb8b6 1623 break;
ba7965c4 1624 }
c0560973 1625 }
2328be1e
JB
1626 }
1627
c0560973 1628 private onPing(): void {
9f2e3130 1629 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
c0560973
JB
1630 }
1631
1632 private onPong(): void {
9f2e3130 1633 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
c0560973
JB
1634 }
1635
9534e74e 1636 private onError(error: WSError): void {
bcc9c3c0 1637 this.closeWSConnection();
32de5a57 1638 logger.error(this.logPrefix() + ' WebSocket error:', error);
c0560973
JB
1639 }
1640
07989fad
JB
1641 private getEnergyActiveImportRegister(
1642 connectorStatus: ConnectorStatus,
1643 meterStop = false
1644 ): number {
95bdbf12 1645 if (this.getMeteringPerTransaction() === true) {
07989fad
JB
1646 return (
1647 (meterStop === true
1648 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1649 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1650 );
1651 }
1652 return (
1653 (meterStop === true
1654 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1655 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1656 );
1657 }
1658
bb83b5ed 1659 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
fa7bccf4
JB
1660 const localStationInfo = stationInfo ?? this.stationInfo;
1661 return !Utils.isUndefined(localStationInfo.useConnectorId0)
1662 ? localStationInfo.useConnectorId0
e7aeea18 1663 : true;
8bce55bf
JB
1664 }
1665
60ddad53
JB
1666 private getNumberOfRunningTransactions(): number {
1667 let trxCount = 0;
1668 for (const connectorId of this.connectors.keys()) {
1669 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1670 trxCount++;
1671 }
1672 }
1673 return trxCount;
1674 }
1675
1676 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1677 for (const connectorId of this.connectors.keys()) {
1678 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1679 await this.stopTransactionOnConnector(connectorId, reason);
1680 }
1681 }
1682 }
1683
1f761b9a 1684 // 0 for disabling
c72f6634 1685 private getConnectionTimeout(): number {
17ac262c
JB
1686 if (
1687 ChargingStationConfigurationUtils.getConfigurationKey(
1688 this,
1689 StandardParametersKey.ConnectionTimeOut
1690 )
1691 ) {
e7aeea18 1692 return (
17ac262c
JB
1693 parseInt(
1694 ChargingStationConfigurationUtils.getConfigurationKey(
1695 this,
1696 StandardParametersKey.ConnectionTimeOut
1697 ).value
1698 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 1699 );
291cb255 1700 }
291cb255 1701 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1702 }
1703
1f761b9a 1704 // -1 for unlimited, 0 for disabling
c72f6634 1705 private getAutoReconnectMaxRetries(): number {
ad2f27c3
JB
1706 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1707 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1708 }
1709 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1710 return Configuration.getAutoReconnectMaxRetries();
1711 }
1712 return -1;
1713 }
1714
ec977daf 1715 // 0 for disabling
c72f6634 1716 private getRegistrationMaxRetries(): number {
ad2f27c3
JB
1717 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1718 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1719 }
1720 return -1;
1721 }
1722
c0560973
JB
1723 private getPowerDivider(): number {
1724 let powerDivider = this.getNumberOfConnectors();
fa7bccf4 1725 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 1726 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1727 }
1728 return powerDivider;
1729 }
1730
fa7bccf4
JB
1731 private getMaximumPower(stationInfo?: ChargingStationInfo): number {
1732 const localStationInfo = stationInfo ?? this.stationInfo;
1733 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
0642c3d2
JB
1734 }
1735
fa7bccf4
JB
1736 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1737 const maximumPower = this.getMaximumPower(stationInfo);
1738 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
1739 case CurrentType.AC:
1740 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 1741 this.getNumberOfPhases(stationInfo),
ad8537a7 1742 maximumPower / this.getNumberOfConnectors(),
fa7bccf4 1743 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
1744 );
1745 case CurrentType.DC:
fa7bccf4 1746 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
1747 }
1748 }
1749
cc6e8ab5
JB
1750 private getAmperageLimitation(): number | undefined {
1751 if (
1752 this.stationInfo.amperageLimitationOcppKey &&
17ac262c
JB
1753 ChargingStationConfigurationUtils.getConfigurationKey(
1754 this,
1755 this.stationInfo.amperageLimitationOcppKey
1756 )
cc6e8ab5
JB
1757 ) {
1758 return (
1759 Utils.convertToInt(
17ac262c
JB
1760 ChargingStationConfigurationUtils.getConfigurationKey(
1761 this,
1762 this.stationInfo.amperageLimitationOcppKey
1763 ).value
1764 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
1765 );
1766 }
1767 }
1768
60ddad53
JB
1769 private getChargingProfilePowerLimit(connectorId: number): number | undefined {
1770 let limit: number, matchingChargingProfile: ChargingProfile;
1771 let chargingProfiles: ChargingProfile[] = [];
1772 // Get charging profiles for connector and sort by stack level
1773 chargingProfiles = this.getConnectorStatus(connectorId).chargingProfiles.sort(
1774 (a, b) => b.stackLevel - a.stackLevel
1775 );
1776 // Get profiles on connector 0
1777 if (this.getConnectorStatus(0).chargingProfiles) {
1778 chargingProfiles.push(
1779 ...this.getConnectorStatus(0).chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
1780 );
1781 }
1782 if (!Utils.isEmptyArray(chargingProfiles)) {
1783 const result = ChargingStationUtils.getLimitFromChargingProfiles(
1784 chargingProfiles,
1785 this.logPrefix()
1786 );
1787 if (!Utils.isNullOrUndefined(result)) {
1788 limit = result.limit;
1789 matchingChargingProfile = result.matchingChargingProfile;
1790 switch (this.getCurrentOutType()) {
1791 case CurrentType.AC:
1792 limit =
1793 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
1794 ChargingRateUnitType.WATT
1795 ? limit
1796 : ACElectricUtils.powerTotal(this.getNumberOfPhases(), this.getVoltageOut(), limit);
1797 break;
1798 case CurrentType.DC:
1799 limit =
1800 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
1801 ChargingRateUnitType.WATT
1802 ? limit
1803 : DCElectricUtils.power(this.getVoltageOut(), limit);
1804 }
1805 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
1806 if (limit > connectorMaximumPower) {
1807 logger.error(
1808 `${this.logPrefix()} Charging profile id ${
1809 matchingChargingProfile.chargingProfileId
45988780 1810 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}, dump charging profiles' stack: %j`,
60ddad53
JB
1811 this.getConnectorStatus(connectorId).chargingProfiles
1812 );
1813 limit = connectorMaximumPower;
1814 }
1815 }
1816 }
1817 return limit;
1818 }
1819
c0560973 1820 private async startMessageSequence(): Promise<void> {
b7f9e41d 1821 if (this.stationInfo?.autoRegister === true) {
f7f98c68 1822 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
1823 BootNotificationRequest,
1824 BootNotificationResponse
8bfbc743
JB
1825 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1826 skipBufferingOnError: true,
1827 });
6114e6f1 1828 }
136c90ba 1829 // Start WebSocket ping
c0560973 1830 this.startWebSocketPing();
5ad8570f 1831 // Start heartbeat
c0560973 1832 this.startHeartbeat();
0a60c33c 1833 // Initialize connectors status
734d790d 1834 for (const connectorId of this.connectors.keys()) {
6e939d9e 1835 let connectorStatus: ConnectorStatusEnum;
734d790d 1836 if (connectorId === 0) {
593cf3f9 1837 continue;
e7aeea18 1838 } else if (
56eb297e
JB
1839 !this.getConnectorStatus(connectorId)?.status &&
1840 (this.isChargingStationAvailable() === false ||
1789ba2c 1841 this.isConnectorAvailable(connectorId) === false)
e7aeea18 1842 ) {
6e939d9e 1843 connectorStatus = ConnectorStatusEnum.UNAVAILABLE;
45c0ae82
JB
1844 } else if (
1845 !this.getConnectorStatus(connectorId)?.status &&
1846 this.getConnectorStatus(connectorId)?.bootStatus
1847 ) {
1848 // Set boot status in template at startup
6e939d9e 1849 connectorStatus = this.getConnectorStatus(connectorId).bootStatus;
56eb297e
JB
1850 } else if (this.getConnectorStatus(connectorId)?.status) {
1851 // Set previous status at startup
6e939d9e 1852 connectorStatus = this.getConnectorStatus(connectorId).status;
5ad8570f 1853 } else {
56eb297e 1854 // Set default status
6e939d9e 1855 connectorStatus = ConnectorStatusEnum.AVAILABLE;
5ad8570f 1856 }
56eb297e
JB
1857 await this.ocppRequestService.requestHandler<
1858 StatusNotificationRequest,
1859 StatusNotificationResponse
6e939d9e
JB
1860 >(
1861 this,
1862 RequestCommand.STATUS_NOTIFICATION,
1863 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1864 );
1865 this.getConnectorStatus(connectorId).status = connectorStatus;
5ad8570f 1866 }
c9a4f9ea
JB
1867 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1868 await this.ocppRequestService.requestHandler<
1869 FirmwareStatusNotificationRequest,
1870 FirmwareStatusNotificationResponse
1871 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1872 status: FirmwareStatus.Installed,
1873 });
1874 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 1875 }
3637ca2c 1876
0a60c33c 1877 // Start the ATG
60ddad53 1878 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
4f69be04 1879 this.startAutomaticTransactionGenerator();
fa7bccf4 1880 }
aa428a31 1881 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
1882 }
1883
e7aeea18
JB
1884 private async stopMessageSequence(
1885 reason: StopTransactionReason = StopTransactionReason.NONE
1886 ): Promise<void> {
136c90ba 1887 // Stop WebSocket ping
c0560973 1888 this.stopWebSocketPing();
79411696 1889 // Stop heartbeat
c0560973 1890 this.stopHeartbeat();
fa7bccf4 1891 // Stop ongoing transactions
b20eb107 1892 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
1893 this.stopAutomaticTransactionGenerator();
1894 } else {
1895 await this.stopRunningTransactions(reason);
79411696 1896 }
45c0ae82
JB
1897 for (const connectorId of this.connectors.keys()) {
1898 if (connectorId > 0) {
1899 await this.ocppRequestService.requestHandler<
1900 StatusNotificationRequest,
1901 StatusNotificationResponse
6e939d9e
JB
1902 >(
1903 this,
1904 RequestCommand.STATUS_NOTIFICATION,
1905 OCPPServiceUtils.buildStatusNotificationRequest(
1906 this,
1907 connectorId,
1908 ConnectorStatusEnum.UNAVAILABLE
1909 )
1910 );
45c0ae82
JB
1911 this.getConnectorStatus(connectorId).status = null;
1912 }
1913 }
79411696
JB
1914 }
1915
c0560973 1916 private startWebSocketPing(): void {
17ac262c
JB
1917 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1918 this,
e7aeea18
JB
1919 StandardParametersKey.WebSocketPingInterval
1920 )
1921 ? Utils.convertToInt(
17ac262c
JB
1922 ChargingStationConfigurationUtils.getConfigurationKey(
1923 this,
1924 StandardParametersKey.WebSocketPingInterval
1925 ).value
e7aeea18 1926 )
9cd3dfb0 1927 : 0;
ad2f27c3
JB
1928 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1929 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 1930 if (this.isWebSocketConnectionOpened() === true) {
db0af086 1931 this.wsConnection.ping();
136c90ba
JB
1932 }
1933 }, webSocketPingInterval * 1000);
e7aeea18
JB
1934 logger.info(
1935 this.logPrefix() +
1936 ' WebSocket ping started every ' +
1937 Utils.formatDurationSeconds(webSocketPingInterval)
1938 );
ad2f27c3 1939 } else if (this.webSocketPingSetInterval) {
e7aeea18
JB
1940 logger.info(
1941 this.logPrefix() +
d56ea27c
JB
1942 ' WebSocket ping already started every ' +
1943 Utils.formatDurationSeconds(webSocketPingInterval)
e7aeea18 1944 );
136c90ba 1945 } else {
e7aeea18
JB
1946 logger.error(
1947 `${this.logPrefix()} WebSocket ping interval set to ${
1948 webSocketPingInterval
1949 ? Utils.formatDurationSeconds(webSocketPingInterval)
1950 : webSocketPingInterval
1951 }, not starting the WebSocket ping`
1952 );
136c90ba
JB
1953 }
1954 }
1955
c0560973 1956 private stopWebSocketPing(): void {
ad2f27c3
JB
1957 if (this.webSocketPingSetInterval) {
1958 clearInterval(this.webSocketPingSetInterval);
136c90ba
JB
1959 }
1960 }
1961
1f5df42a 1962 private getConfiguredSupervisionUrl(): URL {
f70425a5 1963 const supervisionUrls = this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls();
c0560973 1964 if (!Utils.isEmptyArray(supervisionUrls)) {
2dcfe98e
JB
1965 switch (Configuration.getSupervisionUrlDistribution()) {
1966 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634
JB
1967 // FIXME
1968 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e
JB
1969 break;
1970 case SupervisionUrlDistribution.RANDOM:
c72f6634
JB
1971 this.configuredSupervisionUrlIndex = Math.floor(
1972 Utils.secureRandom() * supervisionUrls.length
1973 );
2dcfe98e 1974 break;
c72f6634
JB
1975 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1976 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e
JB
1977 break;
1978 default:
e7aeea18
JB
1979 logger.error(
1980 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
c72f6634 1981 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
e7aeea18
JB
1982 }`
1983 );
c72f6634 1984 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 1985 break;
c0560973 1986 }
c72f6634 1987 return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
c0560973 1988 }
57939a9d 1989 return new URL(supervisionUrls as string);
136c90ba
JB
1990 }
1991
c72f6634 1992 private getHeartbeatInterval(): number {
17ac262c
JB
1993 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1994 this,
1995 StandardParametersKey.HeartbeatInterval
1996 );
c0560973
JB
1997 if (HeartbeatInterval) {
1998 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1999 }
17ac262c
JB
2000 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
2001 this,
2002 StandardParametersKey.HeartBeatInterval
2003 );
c0560973
JB
2004 if (HeartBeatInterval) {
2005 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
0a60c33c 2006 }
b7f9e41d 2007 this.stationInfo?.autoRegister === false &&
e7aeea18
JB
2008 logger.warn(
2009 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
2010 Constants.DEFAULT_HEARTBEAT_INTERVAL
2011 }`
2012 );
47e22477 2013 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
0a60c33c
JB
2014 }
2015
c0560973 2016 private stopHeartbeat(): void {
ad2f27c3
JB
2017 if (this.heartbeatSetInterval) {
2018 clearInterval(this.heartbeatSetInterval);
7dde0b73 2019 }
5ad8570f
JB
2020 }
2021
55516218 2022 private terminateWSConnection(): void {
56eb297e 2023 if (this.isWebSocketConnectionOpened() === true) {
55516218
JB
2024 this.wsConnection.terminate();
2025 this.wsConnection = null;
2026 }
2027 }
2028
dd119a6b 2029 private stopMeterValues(connectorId: number) {
734d790d
JB
2030 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
2031 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
dd119a6b
JB
2032 }
2033 }
2034
c72f6634 2035 private getReconnectExponentialDelay(): boolean {
e7aeea18
JB
2036 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
2037 ? this.stationInfo.reconnectExponentialDelay
2038 : false;
5ad8570f
JB
2039 }
2040
aa428a31 2041 private async reconnect(): Promise<void> {
7874b0b1
JB
2042 // Stop WebSocket ping
2043 this.stopWebSocketPing();
136c90ba 2044 // Stop heartbeat
c0560973 2045 this.stopHeartbeat();
5ad8570f 2046 // Stop the ATG if needed
6d9876e7 2047 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
fa7bccf4 2048 this.stopAutomaticTransactionGenerator();
ad2f27c3 2049 }
e7aeea18
JB
2050 if (
2051 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2052 this.getAutoReconnectMaxRetries() === -1
2053 ) {
ad2f27c3 2054 this.autoReconnectRetryCount++;
e7aeea18
JB
2055 const reconnectDelay = this.getReconnectExponentialDelay()
2056 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2057 : this.getConnectionTimeout() * 1000;
1e080116
JB
2058 const reconnectDelayWithdraw = 1000;
2059 const reconnectTimeout =
2060 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2061 ? reconnectDelay - reconnectDelayWithdraw
2062 : 0;
e7aeea18 2063 logger.error(
d56ea27c 2064 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2065 reconnectDelay,
2066 2
2067 )}ms, timeout ${reconnectTimeout}ms`
2068 );
032d6efc 2069 await Utils.sleep(reconnectDelay);
e7aeea18 2070 logger.error(
d56ea27c 2071 this.logPrefix() + ' WebSocket connection retry #' + this.autoReconnectRetryCount.toString()
e7aeea18
JB
2072 );
2073 this.openWSConnection(
ccb1d6e9 2074 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
1e080116 2075 { closeOpened: true }
e7aeea18 2076 );
265e4266 2077 this.wsConnectionRestarted = true;
c0560973 2078 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2079 logger.error(
d56ea27c 2080 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2081 this.autoReconnectRetryCount
d56ea27c 2082 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2083 );
5ad8570f
JB
2084 }
2085 }
2086
fa7bccf4
JB
2087 private getAutomaticTransactionGeneratorConfigurationFromTemplate(): AutomaticTransactionGeneratorConfiguration | null {
2088 return this.getTemplateFromFile()?.AutomaticTransactionGenerator ?? null;
2089 }
2090
a2653482
JB
2091 private initializeConnectorStatus(connectorId: number): void {
2092 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2093 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2094 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d
JB
2095 this.getConnectorStatus(connectorId).transactionStarted = false;
2096 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2097 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
0a60c33c 2098 }
7dde0b73 2099}