refactor: remove unneeded intermediate variable in id tags cache code
[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 (this.getConnectorStatus(connectorId)?.transactionStarted === undefined ||
1324 this.getConnectorStatus(connectorId)?.transactionStarted === null)
1325 ) {
1326 this.initializeConnectorStatus(connectorId);
1327 }
1328 }
1329 }
1330
1331 private checkStationInfoConnectorStatus(
1332 connectorId: number,
1333 connectorStatus: ConnectorStatus
1334 ): void {
1335 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1336 logger.warn(
1337 `${this.logPrefix()} Charging station information from template ${
1338 this.templateFile
1339 } with connector ${connectorId} status configuration defined, undefine it`
1340 );
1341 delete connectorStatus.status;
1342 }
1343 }
1344
1345 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1346 let configuration: ChargingStationConfiguration | undefined;
1347 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1348 try {
1349 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1350 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1351 this.configurationFileHash
1352 );
1353 } else {
1354 const measureId = `${FileType.ChargingStationConfiguration} read`;
1355 const beginId = PerformanceStatistics.beginMeasure(measureId);
1356 configuration = JSON.parse(
1357 fs.readFileSync(this.configurationFile, 'utf8')
1358 ) as ChargingStationConfiguration;
1359 PerformanceStatistics.endMeasure(measureId, beginId);
1360 this.configurationFileHash = configuration.configurationHash;
1361 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1362 }
1363 } catch (error) {
1364 FileUtils.handleFileException(
1365 this.configurationFile,
1366 FileType.ChargingStationConfiguration,
1367 error as NodeJS.ErrnoException,
1368 this.logPrefix()
1369 );
1370 }
1371 }
1372 return configuration;
1373 }
1374
1375 private saveConfiguration(): void {
1376 if (this.configurationFile) {
1377 try {
1378 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1379 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1380 }
1381 const configurationData: ChargingStationConfiguration =
1382 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
1383 this.ocppConfiguration?.configurationKey &&
1384 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1385 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1386 delete configurationData.configurationHash;
1387 const configurationHash = crypto
1388 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1389 .update(JSON.stringify(configurationData))
1390 .digest('hex');
1391 if (this.configurationFileHash !== configurationHash) {
1392 configurationData.configurationHash = configurationHash;
1393 const measureId = `${FileType.ChargingStationConfiguration} write`;
1394 const beginId = PerformanceStatistics.beginMeasure(measureId);
1395 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1396 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1397 fs.closeSync(fileDescriptor);
1398 PerformanceStatistics.endMeasure(measureId, beginId);
1399 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1400 this.configurationFileHash = configurationHash;
1401 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1402 } else {
1403 logger.debug(
1404 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1405 this.configurationFile
1406 }`
1407 );
1408 }
1409 } catch (error) {
1410 FileUtils.handleFileException(
1411 this.configurationFile,
1412 FileType.ChargingStationConfiguration,
1413 error as NodeJS.ErrnoException,
1414 this.logPrefix()
1415 );
1416 }
1417 } else {
1418 logger.error(
1419 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1420 );
1421 }
1422 }
1423
1424 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1425 return this.getTemplateFromFile()?.Configuration;
1426 }
1427
1428 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1429 let configuration: ChargingStationConfiguration | undefined;
1430 if (this.getOcppPersistentConfiguration() === true) {
1431 const configurationFromFile = this.getConfigurationFromFile();
1432 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1433 }
1434 if (!Utils.isNullOrUndefined(configuration)) {
1435 delete configuration.stationInfo;
1436 delete configuration.configurationHash;
1437 }
1438 return configuration;
1439 }
1440
1441 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1442 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1443 this.getOcppConfigurationFromFile();
1444 if (!ocppConfiguration) {
1445 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1446 }
1447 return ocppConfiguration;
1448 }
1449
1450 private async onOpen(): Promise<void> {
1451 if (this.isWebSocketConnectionOpened() === true) {
1452 logger.info(
1453 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1454 );
1455 if (this.isRegistered() === false) {
1456 // Send BootNotification
1457 let registrationRetryCount = 0;
1458 do {
1459 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1460 BootNotificationRequest,
1461 BootNotificationResponse
1462 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1463 skipBufferingOnError: true,
1464 });
1465 if (this.isRegistered() === false) {
1466 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1467 await Utils.sleep(
1468 this?.bootNotificationResponse?.interval
1469 ? this.bootNotificationResponse.interval * 1000
1470 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1471 );
1472 }
1473 } while (
1474 this.isRegistered() === false &&
1475 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1476 this.getRegistrationMaxRetries() === -1)
1477 );
1478 }
1479 if (this.isRegistered() === true) {
1480 if (this.isInAcceptedState() === true) {
1481 await this.startMessageSequence();
1482 }
1483 } else {
1484 logger.error(
1485 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1486 );
1487 }
1488 this.wsConnectionRestarted = false;
1489 this.autoReconnectRetryCount = 0;
1490 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1491 } else {
1492 logger.warn(
1493 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1494 );
1495 }
1496 }
1497
1498 private async onClose(code: number, reason: Buffer): Promise<void> {
1499 switch (code) {
1500 // Normal close
1501 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1502 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1503 logger.info(
1504 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1505 code
1506 )}' and reason '${reason.toString()}'`
1507 );
1508 this.autoReconnectRetryCount = 0;
1509 break;
1510 // Abnormal close
1511 default:
1512 logger.error(
1513 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1514 code
1515 )}' and reason '${reason.toString()}'`
1516 );
1517 this.started === true && (await this.reconnect());
1518 break;
1519 }
1520 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1521 }
1522
1523 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1524 const cachedRequest = this.requests.get(messageId);
1525 if (Array.isArray(cachedRequest) === true) {
1526 return cachedRequest;
1527 }
1528 throw new OCPPError(
1529 ErrorType.PROTOCOL_ERROR,
1530 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1531 messageType
1532 )} is not an array`,
1533 undefined,
1534 cachedRequest as JsonType
1535 );
1536 }
1537
1538 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1539 const [messageType, messageId, commandName, commandPayload] = request;
1540 if (this.getEnableStatistics() === true) {
1541 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1542 }
1543 logger.debug(
1544 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1545 request
1546 )}`
1547 );
1548 // Process the message
1549 await this.ocppIncomingRequestService.incomingRequestHandler(
1550 this,
1551 messageId,
1552 commandName,
1553 commandPayload
1554 );
1555 }
1556
1557 private handleResponseMessage(response: Response): void {
1558 const [messageType, messageId, commandPayload] = response;
1559 if (this.requests.has(messageId) === false) {
1560 // Error
1561 throw new OCPPError(
1562 ErrorType.INTERNAL_ERROR,
1563 `Response for unknown message id ${messageId}`,
1564 undefined,
1565 commandPayload
1566 );
1567 }
1568 // Respond
1569 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1570 messageType,
1571 messageId
1572 );
1573 logger.debug(
1574 `${this.logPrefix()} << Command '${
1575 requestCommandName ?? Constants.UNKNOWN_COMMAND
1576 }' received response payload: ${JSON.stringify(response)}`
1577 );
1578 responseCallback(commandPayload, requestPayload);
1579 }
1580
1581 private handleErrorMessage(errorResponse: ErrorResponse): void {
1582 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1583 if (this.requests.has(messageId) === false) {
1584 // Error
1585 throw new OCPPError(
1586 ErrorType.INTERNAL_ERROR,
1587 `Error response for unknown message id ${messageId}`,
1588 undefined,
1589 { errorType, errorMessage, errorDetails }
1590 );
1591 }
1592 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1593 logger.debug(
1594 `${this.logPrefix()} << Command '${
1595 requestCommandName ?? Constants.UNKNOWN_COMMAND
1596 }' received error response payload: ${JSON.stringify(errorResponse)}`
1597 );
1598 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1599 }
1600
1601 private async onMessage(data: RawData): Promise<void> {
1602 let request: IncomingRequest | Response | ErrorResponse;
1603 let messageType: number;
1604 let errMsg: string;
1605 try {
1606 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1607 if (Array.isArray(request) === true) {
1608 [messageType] = request;
1609 // Check the type of message
1610 switch (messageType) {
1611 // Incoming Message
1612 case MessageType.CALL_MESSAGE:
1613 await this.handleIncomingMessage(request as IncomingRequest);
1614 break;
1615 // Response Message
1616 case MessageType.CALL_RESULT_MESSAGE:
1617 this.handleResponseMessage(request as Response);
1618 break;
1619 // Error Message
1620 case MessageType.CALL_ERROR_MESSAGE:
1621 this.handleErrorMessage(request as ErrorResponse);
1622 break;
1623 // Unknown Message
1624 default:
1625 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1626 errMsg = `Wrong message type ${messageType}`;
1627 logger.error(`${this.logPrefix()} ${errMsg}`);
1628 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1629 }
1630 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1631 } else {
1632 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1633 request,
1634 });
1635 }
1636 } catch (error) {
1637 let commandName: IncomingRequestCommand;
1638 let requestCommandName: RequestCommand | IncomingRequestCommand;
1639 let errorCallback: ErrorCallback;
1640 const [, messageId] = request;
1641 switch (messageType) {
1642 case MessageType.CALL_MESSAGE:
1643 [, , commandName] = request as IncomingRequest;
1644 // Send error
1645 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1646 break;
1647 case MessageType.CALL_RESULT_MESSAGE:
1648 case MessageType.CALL_ERROR_MESSAGE:
1649 if (this.requests.has(messageId) === true) {
1650 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1651 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1652 errorCallback(error as OCPPError, false);
1653 } else {
1654 // Remove the request from the cache in case of error at response handling
1655 this.requests.delete(messageId);
1656 }
1657 break;
1658 }
1659 if (error instanceof OCPPError === false) {
1660 logger.warn(
1661 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1662 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1663 }' message '${data.toString()}' handling is not an OCPPError:`,
1664 error
1665 );
1666 }
1667 logger.error(
1668 `${this.logPrefix()} Incoming OCPP command '${
1669 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1670 }' message '${data.toString()}'${
1671 messageType !== MessageType.CALL_MESSAGE
1672 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1673 : ''
1674 } processing error:`,
1675 error
1676 );
1677 }
1678 }
1679
1680 private onPing(): void {
1681 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1682 }
1683
1684 private onPong(): void {
1685 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1686 }
1687
1688 private onError(error: WSError): void {
1689 this.closeWSConnection();
1690 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1691 }
1692
1693 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1694 if (this.getMeteringPerTransaction() === true) {
1695 return (
1696 (rounded === true
1697 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1698 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1699 );
1700 }
1701 return (
1702 (rounded === true
1703 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1704 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1705 );
1706 }
1707
1708 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1709 const localStationInfo = stationInfo ?? this.stationInfo;
1710 return localStationInfo?.useConnectorId0 ?? true;
1711 }
1712
1713 private getNumberOfRunningTransactions(): number {
1714 let trxCount = 0;
1715 for (const connectorId of this.connectors.keys()) {
1716 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1717 trxCount++;
1718 }
1719 }
1720 return trxCount;
1721 }
1722
1723 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1724 for (const connectorId of this.connectors.keys()) {
1725 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1726 await this.stopTransactionOnConnector(connectorId, reason);
1727 }
1728 }
1729 }
1730
1731 // 0 for disabling
1732 private getConnectionTimeout(): number {
1733 if (
1734 ChargingStationConfigurationUtils.getConfigurationKey(
1735 this,
1736 StandardParametersKey.ConnectionTimeOut
1737 )
1738 ) {
1739 return (
1740 parseInt(
1741 ChargingStationConfigurationUtils.getConfigurationKey(
1742 this,
1743 StandardParametersKey.ConnectionTimeOut
1744 ).value
1745 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1746 );
1747 }
1748 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1749 }
1750
1751 // -1 for unlimited, 0 for disabling
1752 private getAutoReconnectMaxRetries(): number | undefined {
1753 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1754 return this.stationInfo.autoReconnectMaxRetries;
1755 }
1756 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1757 return Configuration.getAutoReconnectMaxRetries();
1758 }
1759 return -1;
1760 }
1761
1762 // 0 for disabling
1763 private getRegistrationMaxRetries(): number | undefined {
1764 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1765 return this.stationInfo.registrationMaxRetries;
1766 }
1767 return -1;
1768 }
1769
1770 private getPowerDivider(): number {
1771 let powerDivider = this.getNumberOfConnectors();
1772 if (this.stationInfo?.powerSharedByConnectors) {
1773 powerDivider = this.getNumberOfRunningTransactions();
1774 }
1775 return powerDivider;
1776 }
1777
1778 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1779 const maximumPower = this.getMaximumPower(stationInfo);
1780 switch (this.getCurrentOutType(stationInfo)) {
1781 case CurrentType.AC:
1782 return ACElectricUtils.amperagePerPhaseFromPower(
1783 this.getNumberOfPhases(stationInfo),
1784 maximumPower / this.getNumberOfConnectors(),
1785 this.getVoltageOut(stationInfo)
1786 );
1787 case CurrentType.DC:
1788 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1789 }
1790 }
1791
1792 private getAmperageLimitation(): number | undefined {
1793 if (
1794 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1795 ChargingStationConfigurationUtils.getConfigurationKey(
1796 this,
1797 this.stationInfo.amperageLimitationOcppKey
1798 )
1799 ) {
1800 return (
1801 Utils.convertToInt(
1802 ChargingStationConfigurationUtils.getConfigurationKey(
1803 this,
1804 this.stationInfo.amperageLimitationOcppKey
1805 )?.value
1806 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1807 );
1808 }
1809 }
1810
1811 private async startMessageSequence(): Promise<void> {
1812 if (this.stationInfo?.autoRegister === true) {
1813 await this.ocppRequestService.requestHandler<
1814 BootNotificationRequest,
1815 BootNotificationResponse
1816 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1817 skipBufferingOnError: true,
1818 });
1819 }
1820 // Start WebSocket ping
1821 this.startWebSocketPing();
1822 // Start heartbeat
1823 this.startHeartbeat();
1824 // Initialize connectors status
1825 for (const connectorId of this.connectors.keys()) {
1826 let connectorStatus: ConnectorStatusEnum | undefined;
1827 if (connectorId === 0) {
1828 continue;
1829 } else if (
1830 !this.getConnectorStatus(connectorId)?.status &&
1831 (this.isChargingStationAvailable() === false ||
1832 this.isConnectorAvailable(connectorId) === false)
1833 ) {
1834 connectorStatus = ConnectorStatusEnum.Unavailable;
1835 } else if (
1836 !this.getConnectorStatus(connectorId)?.status &&
1837 this.getConnectorStatus(connectorId)?.bootStatus
1838 ) {
1839 // Set boot status in template at startup
1840 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
1841 } else if (this.getConnectorStatus(connectorId)?.status) {
1842 // Set previous status at startup
1843 connectorStatus = this.getConnectorStatus(connectorId)?.status;
1844 } else {
1845 // Set default status
1846 connectorStatus = ConnectorStatusEnum.Available;
1847 }
1848 await this.ocppRequestService.requestHandler<
1849 StatusNotificationRequest,
1850 StatusNotificationResponse
1851 >(
1852 this,
1853 RequestCommand.STATUS_NOTIFICATION,
1854 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1855 );
1856 this.getConnectorStatus(connectorId).status = connectorStatus;
1857 }
1858 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1859 await this.ocppRequestService.requestHandler<
1860 FirmwareStatusNotificationRequest,
1861 FirmwareStatusNotificationResponse
1862 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1863 status: FirmwareStatus.Installed,
1864 });
1865 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
1866 }
1867
1868 // Start the ATG
1869 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
1870 this.startAutomaticTransactionGenerator();
1871 }
1872 this.wsConnectionRestarted === true && this.flushMessageBuffer();
1873 }
1874
1875 private async stopMessageSequence(
1876 reason: StopTransactionReason = StopTransactionReason.NONE
1877 ): Promise<void> {
1878 // Stop WebSocket ping
1879 this.stopWebSocketPing();
1880 // Stop heartbeat
1881 this.stopHeartbeat();
1882 // Stop ongoing transactions
1883 if (this.automaticTransactionGenerator?.started === true) {
1884 this.stopAutomaticTransactionGenerator();
1885 } else {
1886 await this.stopRunningTransactions(reason);
1887 }
1888 for (const connectorId of this.connectors.keys()) {
1889 if (connectorId > 0) {
1890 await this.ocppRequestService.requestHandler<
1891 StatusNotificationRequest,
1892 StatusNotificationResponse
1893 >(
1894 this,
1895 RequestCommand.STATUS_NOTIFICATION,
1896 OCPPServiceUtils.buildStatusNotificationRequest(
1897 this,
1898 connectorId,
1899 ConnectorStatusEnum.Unavailable
1900 )
1901 );
1902 delete this.getConnectorStatus(connectorId)?.status;
1903 }
1904 }
1905 }
1906
1907 private startWebSocketPing(): void {
1908 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1909 this,
1910 StandardParametersKey.WebSocketPingInterval
1911 )
1912 ? Utils.convertToInt(
1913 ChargingStationConfigurationUtils.getConfigurationKey(
1914 this,
1915 StandardParametersKey.WebSocketPingInterval
1916 )?.value
1917 )
1918 : 0;
1919 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1920 this.webSocketPingSetInterval = setInterval(() => {
1921 if (this.isWebSocketConnectionOpened() === true) {
1922 this.wsConnection?.ping();
1923 }
1924 }, webSocketPingInterval * 1000);
1925 logger.info(
1926 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
1927 webSocketPingInterval
1928 )}`
1929 );
1930 } else if (this.webSocketPingSetInterval) {
1931 logger.info(
1932 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
1933 webSocketPingInterval
1934 )}`
1935 );
1936 } else {
1937 logger.error(
1938 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
1939 );
1940 }
1941 }
1942
1943 private stopWebSocketPing(): void {
1944 if (this.webSocketPingSetInterval) {
1945 clearInterval(this.webSocketPingSetInterval);
1946 delete this.webSocketPingSetInterval;
1947 }
1948 }
1949
1950 private getConfiguredSupervisionUrl(): URL {
1951 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
1952 if (Utils.isNotEmptyArray(supervisionUrls)) {
1953 let configuredSupervisionUrlIndex: number;
1954 switch (Configuration.getSupervisionUrlDistribution()) {
1955 case SupervisionUrlDistribution.RANDOM:
1956 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1957 break;
1958 case SupervisionUrlDistribution.ROUND_ROBIN:
1959 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1960 default:
1961 Object.values(SupervisionUrlDistribution).includes(
1962 Configuration.getSupervisionUrlDistribution()
1963 ) === false &&
1964 logger.error(
1965 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1966 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
1967 }`
1968 );
1969 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1970 break;
1971 }
1972 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
1973 }
1974 return new URL(supervisionUrls as string);
1975 }
1976
1977 private stopHeartbeat(): void {
1978 if (this.heartbeatSetInterval) {
1979 clearInterval(this.heartbeatSetInterval);
1980 delete this.heartbeatSetInterval;
1981 }
1982 }
1983
1984 private terminateWSConnection(): void {
1985 if (this.isWebSocketConnectionOpened() === true) {
1986 this.wsConnection?.terminate();
1987 this.wsConnection = null;
1988 }
1989 }
1990
1991 private stopMeterValues(connectorId: number) {
1992 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1993 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
1994 }
1995 }
1996
1997 private getReconnectExponentialDelay(): boolean {
1998 return this.stationInfo?.reconnectExponentialDelay ?? false;
1999 }
2000
2001 private async reconnect(): Promise<void> {
2002 // Stop WebSocket ping
2003 this.stopWebSocketPing();
2004 // Stop heartbeat
2005 this.stopHeartbeat();
2006 // Stop the ATG if needed
2007 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2008 this.stopAutomaticTransactionGenerator();
2009 }
2010 if (
2011 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2012 this.getAutoReconnectMaxRetries() === -1
2013 ) {
2014 this.autoReconnectRetryCount++;
2015 const reconnectDelay = this.getReconnectExponentialDelay()
2016 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2017 : this.getConnectionTimeout() * 1000;
2018 const reconnectDelayWithdraw = 1000;
2019 const reconnectTimeout =
2020 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2021 ? reconnectDelay - reconnectDelayWithdraw
2022 : 0;
2023 logger.error(
2024 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2025 reconnectDelay,
2026 2
2027 )}ms, timeout ${reconnectTimeout}ms`
2028 );
2029 await Utils.sleep(reconnectDelay);
2030 logger.error(
2031 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2032 );
2033 this.openWSConnection(
2034 {
2035 ...(this.stationInfo?.wsOptions ?? {}),
2036 handshakeTimeout: reconnectTimeout,
2037 },
2038 { closeOpened: true }
2039 );
2040 this.wsConnectionRestarted = true;
2041 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2042 logger.error(
2043 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2044 this.autoReconnectRetryCount
2045 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2046 );
2047 }
2048 }
2049
2050 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2051 | AutomaticTransactionGeneratorConfiguration
2052 | undefined {
2053 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
2054 }
2055
2056 private initializeConnectorStatus(connectorId: number): void {
2057 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2058 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2059 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
2060 this.getConnectorStatus(connectorId).transactionStarted = false;
2061 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2062 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
2063 }
2064 }