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