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