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