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