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