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