92f15e4c20f7c71fdd5f1fffe55723eec306532e
[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 for (const message of this.messageBuffer.values()) {
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:
966 // 1. charging station info from template
967 // 2. charging station info from configuration file
968 // 3. charging station info attribute
969 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
970 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
971 return this.stationInfo;
972 }
973 return stationInfoFromFile;
974 }
975 stationInfoFromFile &&
976 ChargingStationUtils.propagateSerialNumber(
977 this.getTemplateFromFile(),
978 stationInfoFromFile,
979 stationInfoFromTemplate
980 );
981 return stationInfoFromTemplate;
982 }
983
984 private saveStationInfo(): void {
985 if (this.getStationInfoPersistentConfiguration()) {
986 this.saveConfiguration();
987 }
988 }
989
990 private getOcppPersistentConfiguration(): boolean {
991 return this.stationInfo?.ocppPersistentConfiguration ?? true;
992 }
993
994 private getStationInfoPersistentConfiguration(): boolean {
995 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
996 }
997
998 private handleUnsupportedVersion(version: OCPPVersion) {
999 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
1000 logger.error(`${this.logPrefix()} ${errMsg}`);
1001 throw new BaseError(errMsg);
1002 }
1003
1004 private initialize(): void {
1005 this.configurationFile = path.join(
1006 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
1007 `${ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile())}.json`
1008 );
1009 this.stationInfo = this.getStationInfo();
1010 if (
1011 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1012 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1013 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1014 ) {
1015 const patternGroup: number | undefined =
1016 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1017 this.stationInfo.firmwareVersion?.split('.').length;
1018 const match = this.stationInfo?.firmwareVersion
1019 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1020 ?.slice(1, patternGroup + 1);
1021 const patchLevelIndex = match.length - 1;
1022 match[patchLevelIndex] = (
1023 Utils.convertToInt(match[patchLevelIndex]) +
1024 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
1025 ).toString();
1026 this.stationInfo.firmwareVersion = match?.join('.');
1027 }
1028 this.saveStationInfo();
1029 // Avoid duplication of connectors related information in RAM
1030 delete this.stationInfo?.Connectors;
1031 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1032 if (this.getEnableStatistics() === true) {
1033 this.performanceStatistics = PerformanceStatistics.getInstance(
1034 this.stationInfo.hashId,
1035 this.stationInfo.chargingStationId,
1036 this.configuredSupervisionUrl
1037 );
1038 }
1039 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1040 this.stationInfo
1041 );
1042 this.powerDivider = this.getPowerDivider();
1043 // OCPP configuration
1044 this.ocppConfiguration = this.getOcppConfiguration();
1045 this.initializeOcppConfiguration();
1046 this.initializeOcppServices();
1047 if (this.stationInfo?.autoRegister === true) {
1048 this.bootNotificationResponse = {
1049 currentTime: new Date(),
1050 interval: this.getHeartbeatInterval() / 1000,
1051 status: RegistrationStatusEnumType.ACCEPTED,
1052 };
1053 }
1054 }
1055
1056 private initializeOcppServices(): void {
1057 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1058 switch (ocppVersion) {
1059 case OCPPVersion.VERSION_16:
1060 this.ocppIncomingRequestService =
1061 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1062 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1063 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1064 );
1065 break;
1066 case OCPPVersion.VERSION_20:
1067 case OCPPVersion.VERSION_201:
1068 this.ocppIncomingRequestService =
1069 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1070 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1071 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1072 );
1073 break;
1074 default:
1075 this.handleUnsupportedVersion(ocppVersion);
1076 break;
1077 }
1078 }
1079
1080 private initializeOcppConfiguration(): void {
1081 if (
1082 !ChargingStationConfigurationUtils.getConfigurationKey(
1083 this,
1084 StandardParametersKey.HeartbeatInterval
1085 )
1086 ) {
1087 ChargingStationConfigurationUtils.addConfigurationKey(
1088 this,
1089 StandardParametersKey.HeartbeatInterval,
1090 '0'
1091 );
1092 }
1093 if (
1094 !ChargingStationConfigurationUtils.getConfigurationKey(
1095 this,
1096 StandardParametersKey.HeartBeatInterval
1097 )
1098 ) {
1099 ChargingStationConfigurationUtils.addConfigurationKey(
1100 this,
1101 StandardParametersKey.HeartBeatInterval,
1102 '0',
1103 { visible: false }
1104 );
1105 }
1106 if (
1107 this.getSupervisionUrlOcppConfiguration() &&
1108 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1109 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1110 ) {
1111 ChargingStationConfigurationUtils.addConfigurationKey(
1112 this,
1113 this.getSupervisionUrlOcppKey(),
1114 this.configuredSupervisionUrl.href,
1115 { reboot: true }
1116 );
1117 } else if (
1118 !this.getSupervisionUrlOcppConfiguration() &&
1119 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1120 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1121 ) {
1122 ChargingStationConfigurationUtils.deleteConfigurationKey(
1123 this,
1124 this.getSupervisionUrlOcppKey(),
1125 { save: false }
1126 );
1127 }
1128 if (
1129 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1130 !ChargingStationConfigurationUtils.getConfigurationKey(
1131 this,
1132 this.stationInfo.amperageLimitationOcppKey
1133 )
1134 ) {
1135 ChargingStationConfigurationUtils.addConfigurationKey(
1136 this,
1137 this.stationInfo.amperageLimitationOcppKey,
1138 (
1139 this.stationInfo.maximumAmperage *
1140 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1141 ).toString()
1142 );
1143 }
1144 if (
1145 !ChargingStationConfigurationUtils.getConfigurationKey(
1146 this,
1147 StandardParametersKey.SupportedFeatureProfiles
1148 )
1149 ) {
1150 ChargingStationConfigurationUtils.addConfigurationKey(
1151 this,
1152 StandardParametersKey.SupportedFeatureProfiles,
1153 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1154 );
1155 }
1156 ChargingStationConfigurationUtils.addConfigurationKey(
1157 this,
1158 StandardParametersKey.NumberOfConnectors,
1159 this.getNumberOfConnectors().toString(),
1160 { readonly: true },
1161 { overwrite: true }
1162 );
1163 if (
1164 !ChargingStationConfigurationUtils.getConfigurationKey(
1165 this,
1166 StandardParametersKey.MeterValuesSampledData
1167 )
1168 ) {
1169 ChargingStationConfigurationUtils.addConfigurationKey(
1170 this,
1171 StandardParametersKey.MeterValuesSampledData,
1172 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1173 );
1174 }
1175 if (
1176 !ChargingStationConfigurationUtils.getConfigurationKey(
1177 this,
1178 StandardParametersKey.ConnectorPhaseRotation
1179 )
1180 ) {
1181 const connectorPhaseRotation = [];
1182 for (const connectorId of this.connectors.keys()) {
1183 // AC/DC
1184 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1185 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1186 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1187 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1188 // AC
1189 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1190 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1191 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1192 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1193 }
1194 }
1195 ChargingStationConfigurationUtils.addConfigurationKey(
1196 this,
1197 StandardParametersKey.ConnectorPhaseRotation,
1198 connectorPhaseRotation.toString()
1199 );
1200 }
1201 if (
1202 !ChargingStationConfigurationUtils.getConfigurationKey(
1203 this,
1204 StandardParametersKey.AuthorizeRemoteTxRequests
1205 )
1206 ) {
1207 ChargingStationConfigurationUtils.addConfigurationKey(
1208 this,
1209 StandardParametersKey.AuthorizeRemoteTxRequests,
1210 'true'
1211 );
1212 }
1213 if (
1214 !ChargingStationConfigurationUtils.getConfigurationKey(
1215 this,
1216 StandardParametersKey.LocalAuthListEnabled
1217 ) &&
1218 ChargingStationConfigurationUtils.getConfigurationKey(
1219 this,
1220 StandardParametersKey.SupportedFeatureProfiles
1221 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1222 ) {
1223 ChargingStationConfigurationUtils.addConfigurationKey(
1224 this,
1225 StandardParametersKey.LocalAuthListEnabled,
1226 'false'
1227 );
1228 }
1229 if (
1230 !ChargingStationConfigurationUtils.getConfigurationKey(
1231 this,
1232 StandardParametersKey.ConnectionTimeOut
1233 )
1234 ) {
1235 ChargingStationConfigurationUtils.addConfigurationKey(
1236 this,
1237 StandardParametersKey.ConnectionTimeOut,
1238 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1239 );
1240 }
1241 this.saveOcppConfiguration();
1242 }
1243
1244 private initializeConnectors(
1245 stationInfo: ChargingStationInfo,
1246 configuredMaxConnectors: number,
1247 templateMaxConnectors: number
1248 ): void {
1249 if (!stationInfo?.Connectors && this.connectors.size === 0) {
1250 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1251 logger.error(`${this.logPrefix()} ${logMsg}`);
1252 throw new BaseError(logMsg);
1253 }
1254 if (!stationInfo?.Connectors[0]) {
1255 logger.warn(
1256 `${this.logPrefix()} Charging station information from template ${
1257 this.templateFile
1258 } with no connector Id 0 configuration`
1259 );
1260 }
1261 if (stationInfo?.Connectors) {
1262 const connectorsConfigHash = crypto
1263 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1264 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
1265 .digest('hex');
1266 const connectorsConfigChanged =
1267 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1268 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1269 connectorsConfigChanged && this.connectors.clear();
1270 this.connectorsConfigurationHash = connectorsConfigHash;
1271 // Add connector Id 0
1272 let lastConnector = '0';
1273 for (lastConnector in stationInfo?.Connectors) {
1274 const connectorStatus = stationInfo?.Connectors[lastConnector];
1275 const lastConnectorId = Utils.convertToInt(lastConnector);
1276 if (
1277 lastConnectorId === 0 &&
1278 this.getUseConnectorId0(stationInfo) === true &&
1279 connectorStatus
1280 ) {
1281 this.checkStationInfoConnectorStatus(lastConnectorId, connectorStatus);
1282 this.connectors.set(
1283 lastConnectorId,
1284 Utils.cloneObject<ConnectorStatus>(connectorStatus)
1285 );
1286 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1287 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1288 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1289 }
1290 }
1291 }
1292 // Generate all connectors
1293 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
1294 for (let index = 1; index <= configuredMaxConnectors; index++) {
1295 const randConnectorId = stationInfo?.randomConnectors
1296 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1297 : index;
1298 const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
1299 this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
1300 this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1301 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1302 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1303 this.getConnectorStatus(index).chargingProfiles = [];
1304 }
1305 }
1306 }
1307 }
1308 } else {
1309 logger.warn(
1310 `${this.logPrefix()} Charging station information from template ${
1311 this.templateFile
1312 } with no connectors configuration defined, using already defined connectors`
1313 );
1314 }
1315 // Initialize transaction attributes on connectors
1316 for (const connectorId of this.connectors.keys()) {
1317 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1318 logger.warn(
1319 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
1320 this.getConnectorStatus(connectorId)?.transactionId
1321 }`
1322 );
1323 }
1324 if (
1325 connectorId > 0 &&
1326 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionStarted)
1327 ) {
1328 this.initializeConnectorStatus(connectorId);
1329 }
1330 }
1331 }
1332
1333 private checkStationInfoConnectorStatus(
1334 connectorId: number,
1335 connectorStatus: ConnectorStatus
1336 ): void {
1337 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1338 logger.warn(
1339 `${this.logPrefix()} Charging station information from template ${
1340 this.templateFile
1341 } with connector ${connectorId} status configuration defined, undefine it`
1342 );
1343 delete connectorStatus.status;
1344 }
1345 }
1346
1347 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1348 let configuration: ChargingStationConfiguration | undefined;
1349 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1350 try {
1351 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1352 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1353 this.configurationFileHash
1354 );
1355 } else {
1356 const measureId = `${FileType.ChargingStationConfiguration} read`;
1357 const beginId = PerformanceStatistics.beginMeasure(measureId);
1358 configuration = JSON.parse(
1359 fs.readFileSync(this.configurationFile, 'utf8')
1360 ) as ChargingStationConfiguration;
1361 PerformanceStatistics.endMeasure(measureId, beginId);
1362 this.configurationFileHash = configuration.configurationHash;
1363 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1364 }
1365 } catch (error) {
1366 FileUtils.handleFileException(
1367 this.configurationFile,
1368 FileType.ChargingStationConfiguration,
1369 error as NodeJS.ErrnoException,
1370 this.logPrefix()
1371 );
1372 }
1373 }
1374 return configuration;
1375 }
1376
1377 private saveConfiguration(): void {
1378 if (this.configurationFile) {
1379 try {
1380 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1381 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1382 }
1383 const configurationData: ChargingStationConfiguration =
1384 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
1385 this.ocppConfiguration?.configurationKey &&
1386 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1387 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1388 delete configurationData.configurationHash;
1389 const configurationHash = crypto
1390 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1391 .update(JSON.stringify(configurationData))
1392 .digest('hex');
1393 if (this.configurationFileHash !== configurationHash) {
1394 configurationData.configurationHash = configurationHash;
1395 const measureId = `${FileType.ChargingStationConfiguration} write`;
1396 const beginId = PerformanceStatistics.beginMeasure(measureId);
1397 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1398 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1399 fs.closeSync(fileDescriptor);
1400 PerformanceStatistics.endMeasure(measureId, beginId);
1401 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1402 this.configurationFileHash = configurationHash;
1403 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1404 } else {
1405 logger.debug(
1406 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1407 this.configurationFile
1408 }`
1409 );
1410 }
1411 } catch (error) {
1412 FileUtils.handleFileException(
1413 this.configurationFile,
1414 FileType.ChargingStationConfiguration,
1415 error as NodeJS.ErrnoException,
1416 this.logPrefix()
1417 );
1418 }
1419 } else {
1420 logger.error(
1421 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1422 );
1423 }
1424 }
1425
1426 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1427 return this.getTemplateFromFile()?.Configuration;
1428 }
1429
1430 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1431 let configuration: ChargingStationConfiguration | undefined;
1432 if (this.getOcppPersistentConfiguration() === true) {
1433 const configurationFromFile = this.getConfigurationFromFile();
1434 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1435 }
1436 if (!Utils.isNullOrUndefined(configuration)) {
1437 delete configuration.stationInfo;
1438 delete configuration.configurationHash;
1439 }
1440 return configuration;
1441 }
1442
1443 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1444 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1445 this.getOcppConfigurationFromFile();
1446 if (!ocppConfiguration) {
1447 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1448 }
1449 return ocppConfiguration;
1450 }
1451
1452 private async onOpen(): Promise<void> {
1453 if (this.isWebSocketConnectionOpened() === true) {
1454 logger.info(
1455 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1456 );
1457 if (this.isRegistered() === false) {
1458 // Send BootNotification
1459 let registrationRetryCount = 0;
1460 do {
1461 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1462 BootNotificationRequest,
1463 BootNotificationResponse
1464 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1465 skipBufferingOnError: true,
1466 });
1467 if (this.isRegistered() === false) {
1468 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1469 await Utils.sleep(
1470 this?.bootNotificationResponse?.interval
1471 ? this.bootNotificationResponse.interval * 1000
1472 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1473 );
1474 }
1475 } while (
1476 this.isRegistered() === false &&
1477 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1478 this.getRegistrationMaxRetries() === -1)
1479 );
1480 }
1481 if (this.isRegistered() === true) {
1482 if (this.isInAcceptedState() === true) {
1483 await this.startMessageSequence();
1484 }
1485 } else {
1486 logger.error(
1487 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1488 );
1489 }
1490 this.wsConnectionRestarted = false;
1491 this.autoReconnectRetryCount = 0;
1492 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1493 } else {
1494 logger.warn(
1495 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1496 );
1497 }
1498 }
1499
1500 private async onClose(code: number, reason: Buffer): Promise<void> {
1501 switch (code) {
1502 // Normal close
1503 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1504 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1505 logger.info(
1506 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1507 code
1508 )}' and reason '${reason.toString()}'`
1509 );
1510 this.autoReconnectRetryCount = 0;
1511 break;
1512 // Abnormal close
1513 default:
1514 logger.error(
1515 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1516 code
1517 )}' and reason '${reason.toString()}'`
1518 );
1519 this.started === true && (await this.reconnect());
1520 break;
1521 }
1522 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1523 }
1524
1525 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1526 const cachedRequest = this.requests.get(messageId);
1527 if (Array.isArray(cachedRequest) === true) {
1528 return cachedRequest;
1529 }
1530 throw new OCPPError(
1531 ErrorType.PROTOCOL_ERROR,
1532 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1533 messageType
1534 )} is not an array`,
1535 undefined,
1536 cachedRequest as JsonType
1537 );
1538 }
1539
1540 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1541 const [messageType, messageId, commandName, commandPayload] = request;
1542 if (this.getEnableStatistics() === true) {
1543 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1544 }
1545 logger.debug(
1546 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1547 request
1548 )}`
1549 );
1550 // Process the message
1551 await this.ocppIncomingRequestService.incomingRequestHandler(
1552 this,
1553 messageId,
1554 commandName,
1555 commandPayload
1556 );
1557 }
1558
1559 private handleResponseMessage(response: Response): void {
1560 const [messageType, messageId, commandPayload] = response;
1561 if (this.requests.has(messageId) === false) {
1562 // Error
1563 throw new OCPPError(
1564 ErrorType.INTERNAL_ERROR,
1565 `Response for unknown message id ${messageId}`,
1566 undefined,
1567 commandPayload
1568 );
1569 }
1570 // Respond
1571 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1572 messageType,
1573 messageId
1574 );
1575 logger.debug(
1576 `${this.logPrefix()} << Command '${
1577 requestCommandName ?? Constants.UNKNOWN_COMMAND
1578 }' received response payload: ${JSON.stringify(response)}`
1579 );
1580 responseCallback(commandPayload, requestPayload);
1581 }
1582
1583 private handleErrorMessage(errorResponse: ErrorResponse): void {
1584 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1585 if (this.requests.has(messageId) === false) {
1586 // Error
1587 throw new OCPPError(
1588 ErrorType.INTERNAL_ERROR,
1589 `Error response for unknown message id ${messageId}`,
1590 undefined,
1591 { errorType, errorMessage, errorDetails }
1592 );
1593 }
1594 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1595 logger.debug(
1596 `${this.logPrefix()} << Command '${
1597 requestCommandName ?? Constants.UNKNOWN_COMMAND
1598 }' received error response payload: ${JSON.stringify(errorResponse)}`
1599 );
1600 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1601 }
1602
1603 private async onMessage(data: RawData): Promise<void> {
1604 let request: IncomingRequest | Response | ErrorResponse;
1605 let messageType: number;
1606 let errMsg: string;
1607 try {
1608 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1609 if (Array.isArray(request) === true) {
1610 [messageType] = request;
1611 // Check the type of message
1612 switch (messageType) {
1613 // Incoming Message
1614 case MessageType.CALL_MESSAGE:
1615 await this.handleIncomingMessage(request as IncomingRequest);
1616 break;
1617 // Response Message
1618 case MessageType.CALL_RESULT_MESSAGE:
1619 this.handleResponseMessage(request as Response);
1620 break;
1621 // Error Message
1622 case MessageType.CALL_ERROR_MESSAGE:
1623 this.handleErrorMessage(request as ErrorResponse);
1624 break;
1625 // Unknown Message
1626 default:
1627 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1628 errMsg = `Wrong message type ${messageType}`;
1629 logger.error(`${this.logPrefix()} ${errMsg}`);
1630 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1631 }
1632 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1633 } else {
1634 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1635 request,
1636 });
1637 }
1638 } catch (error) {
1639 let commandName: IncomingRequestCommand;
1640 let requestCommandName: RequestCommand | IncomingRequestCommand;
1641 let errorCallback: ErrorCallback;
1642 const [, messageId] = request;
1643 switch (messageType) {
1644 case MessageType.CALL_MESSAGE:
1645 [, , commandName] = request as IncomingRequest;
1646 // Send error
1647 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1648 break;
1649 case MessageType.CALL_RESULT_MESSAGE:
1650 case MessageType.CALL_ERROR_MESSAGE:
1651 if (this.requests.has(messageId) === true) {
1652 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1653 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1654 errorCallback(error as OCPPError, false);
1655 } else {
1656 // Remove the request from the cache in case of error at response handling
1657 this.requests.delete(messageId);
1658 }
1659 break;
1660 }
1661 if (error instanceof OCPPError === false) {
1662 logger.warn(
1663 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1664 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1665 }' message '${data.toString()}' handling is not an OCPPError:`,
1666 error
1667 );
1668 }
1669 logger.error(
1670 `${this.logPrefix()} Incoming OCPP command '${
1671 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1672 }' message '${data.toString()}'${
1673 messageType !== MessageType.CALL_MESSAGE
1674 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1675 : ''
1676 } processing error:`,
1677 error
1678 );
1679 }
1680 }
1681
1682 private onPing(): void {
1683 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1684 }
1685
1686 private onPong(): void {
1687 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1688 }
1689
1690 private onError(error: WSError): void {
1691 this.closeWSConnection();
1692 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1693 }
1694
1695 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1696 if (this.getMeteringPerTransaction() === true) {
1697 return (
1698 (rounded === true
1699 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1700 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1701 );
1702 }
1703 return (
1704 (rounded === true
1705 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1706 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1707 );
1708 }
1709
1710 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1711 const localStationInfo = stationInfo ?? this.stationInfo;
1712 return localStationInfo?.useConnectorId0 ?? true;
1713 }
1714
1715 private getNumberOfRunningTransactions(): number {
1716 let trxCount = 0;
1717 for (const connectorId of this.connectors.keys()) {
1718 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1719 trxCount++;
1720 }
1721 }
1722 return trxCount;
1723 }
1724
1725 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1726 for (const connectorId of this.connectors.keys()) {
1727 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1728 await this.stopTransactionOnConnector(connectorId, reason);
1729 }
1730 }
1731 }
1732
1733 // 0 for disabling
1734 private getConnectionTimeout(): number {
1735 if (
1736 ChargingStationConfigurationUtils.getConfigurationKey(
1737 this,
1738 StandardParametersKey.ConnectionTimeOut
1739 )
1740 ) {
1741 return (
1742 parseInt(
1743 ChargingStationConfigurationUtils.getConfigurationKey(
1744 this,
1745 StandardParametersKey.ConnectionTimeOut
1746 ).value
1747 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1748 );
1749 }
1750 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1751 }
1752
1753 // -1 for unlimited, 0 for disabling
1754 private getAutoReconnectMaxRetries(): number | undefined {
1755 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1756 return this.stationInfo.autoReconnectMaxRetries;
1757 }
1758 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1759 return Configuration.getAutoReconnectMaxRetries();
1760 }
1761 return -1;
1762 }
1763
1764 // 0 for disabling
1765 private getRegistrationMaxRetries(): number | undefined {
1766 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1767 return this.stationInfo.registrationMaxRetries;
1768 }
1769 return -1;
1770 }
1771
1772 private getPowerDivider(): number {
1773 let powerDivider = this.getNumberOfConnectors();
1774 if (this.stationInfo?.powerSharedByConnectors) {
1775 powerDivider = this.getNumberOfRunningTransactions();
1776 }
1777 return powerDivider;
1778 }
1779
1780 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1781 const maximumPower = this.getMaximumPower(stationInfo);
1782 switch (this.getCurrentOutType(stationInfo)) {
1783 case CurrentType.AC:
1784 return ACElectricUtils.amperagePerPhaseFromPower(
1785 this.getNumberOfPhases(stationInfo),
1786 maximumPower / this.getNumberOfConnectors(),
1787 this.getVoltageOut(stationInfo)
1788 );
1789 case CurrentType.DC:
1790 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1791 }
1792 }
1793
1794 private getAmperageLimitation(): number | undefined {
1795 if (
1796 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1797 ChargingStationConfigurationUtils.getConfigurationKey(
1798 this,
1799 this.stationInfo.amperageLimitationOcppKey
1800 )
1801 ) {
1802 return (
1803 Utils.convertToInt(
1804 ChargingStationConfigurationUtils.getConfigurationKey(
1805 this,
1806 this.stationInfo.amperageLimitationOcppKey
1807 )?.value
1808 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1809 );
1810 }
1811 }
1812
1813 private async startMessageSequence(): Promise<void> {
1814 if (this.stationInfo?.autoRegister === true) {
1815 await this.ocppRequestService.requestHandler<
1816 BootNotificationRequest,
1817 BootNotificationResponse
1818 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1819 skipBufferingOnError: true,
1820 });
1821 }
1822 // Start WebSocket ping
1823 this.startWebSocketPing();
1824 // Start heartbeat
1825 this.startHeartbeat();
1826 // Initialize connectors status
1827 for (const connectorId of this.connectors.keys()) {
1828 let connectorStatus: ConnectorStatusEnum | undefined;
1829 if (connectorId === 0) {
1830 continue;
1831 } else if (
1832 !this.getConnectorStatus(connectorId)?.status &&
1833 (this.isChargingStationAvailable() === false ||
1834 this.isConnectorAvailable(connectorId) === false)
1835 ) {
1836 connectorStatus = ConnectorStatusEnum.Unavailable;
1837 } else if (
1838 !this.getConnectorStatus(connectorId)?.status &&
1839 this.getConnectorStatus(connectorId)?.bootStatus
1840 ) {
1841 // Set boot status in template at startup
1842 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
1843 } else if (this.getConnectorStatus(connectorId)?.status) {
1844 // Set previous status at startup
1845 connectorStatus = this.getConnectorStatus(connectorId)?.status;
1846 } else {
1847 // Set default status
1848 connectorStatus = ConnectorStatusEnum.Available;
1849 }
1850 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorStatus);
1851 }
1852 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1853 await this.ocppRequestService.requestHandler<
1854 FirmwareStatusNotificationRequest,
1855 FirmwareStatusNotificationResponse
1856 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1857 status: FirmwareStatus.Installed,
1858 });
1859 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
1860 }
1861
1862 // Start the ATG
1863 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
1864 this.startAutomaticTransactionGenerator();
1865 }
1866 this.wsConnectionRestarted === true && this.flushMessageBuffer();
1867 }
1868
1869 private async stopMessageSequence(
1870 reason: StopTransactionReason = StopTransactionReason.NONE
1871 ): Promise<void> {
1872 // Stop WebSocket ping
1873 this.stopWebSocketPing();
1874 // Stop heartbeat
1875 this.stopHeartbeat();
1876 // Stop ongoing transactions
1877 if (this.automaticTransactionGenerator?.started === true) {
1878 this.stopAutomaticTransactionGenerator();
1879 } else {
1880 await this.stopRunningTransactions(reason);
1881 }
1882 for (const connectorId of this.connectors.keys()) {
1883 if (connectorId > 0) {
1884 await this.ocppRequestService.requestHandler<
1885 StatusNotificationRequest,
1886 StatusNotificationResponse
1887 >(
1888 this,
1889 RequestCommand.STATUS_NOTIFICATION,
1890 OCPPServiceUtils.buildStatusNotificationRequest(
1891 this,
1892 connectorId,
1893 ConnectorStatusEnum.Unavailable
1894 )
1895 );
1896 delete this.getConnectorStatus(connectorId)?.status;
1897 }
1898 }
1899 }
1900
1901 private startWebSocketPing(): void {
1902 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1903 this,
1904 StandardParametersKey.WebSocketPingInterval
1905 )
1906 ? Utils.convertToInt(
1907 ChargingStationConfigurationUtils.getConfigurationKey(
1908 this,
1909 StandardParametersKey.WebSocketPingInterval
1910 )?.value
1911 )
1912 : 0;
1913 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1914 this.webSocketPingSetInterval = setInterval(() => {
1915 if (this.isWebSocketConnectionOpened() === true) {
1916 this.wsConnection?.ping();
1917 }
1918 }, webSocketPingInterval * 1000);
1919 logger.info(
1920 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
1921 webSocketPingInterval
1922 )}`
1923 );
1924 } else if (this.webSocketPingSetInterval) {
1925 logger.info(
1926 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
1927 webSocketPingInterval
1928 )}`
1929 );
1930 } else {
1931 logger.error(
1932 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
1933 );
1934 }
1935 }
1936
1937 private stopWebSocketPing(): void {
1938 if (this.webSocketPingSetInterval) {
1939 clearInterval(this.webSocketPingSetInterval);
1940 delete this.webSocketPingSetInterval;
1941 }
1942 }
1943
1944 private getConfiguredSupervisionUrl(): URL {
1945 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
1946 if (Utils.isNotEmptyArray(supervisionUrls)) {
1947 let configuredSupervisionUrlIndex: number;
1948 switch (Configuration.getSupervisionUrlDistribution()) {
1949 case SupervisionUrlDistribution.RANDOM:
1950 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1951 break;
1952 case SupervisionUrlDistribution.ROUND_ROBIN:
1953 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1954 default:
1955 Object.values(SupervisionUrlDistribution).includes(
1956 Configuration.getSupervisionUrlDistribution()
1957 ) === false &&
1958 logger.error(
1959 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1960 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
1961 }`
1962 );
1963 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1964 break;
1965 }
1966 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
1967 }
1968 return new URL(supervisionUrls as string);
1969 }
1970
1971 private stopHeartbeat(): void {
1972 if (this.heartbeatSetInterval) {
1973 clearInterval(this.heartbeatSetInterval);
1974 delete this.heartbeatSetInterval;
1975 }
1976 }
1977
1978 private terminateWSConnection(): void {
1979 if (this.isWebSocketConnectionOpened() === true) {
1980 this.wsConnection?.terminate();
1981 this.wsConnection = null;
1982 }
1983 }
1984
1985 private stopMeterValues(connectorId: number) {
1986 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1987 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
1988 }
1989 }
1990
1991 private getReconnectExponentialDelay(): boolean {
1992 return this.stationInfo?.reconnectExponentialDelay ?? false;
1993 }
1994
1995 private async reconnect(): Promise<void> {
1996 // Stop WebSocket ping
1997 this.stopWebSocketPing();
1998 // Stop heartbeat
1999 this.stopHeartbeat();
2000 // Stop the ATG if needed
2001 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2002 this.stopAutomaticTransactionGenerator();
2003 }
2004 if (
2005 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2006 this.getAutoReconnectMaxRetries() === -1
2007 ) {
2008 this.autoReconnectRetryCount++;
2009 const reconnectDelay = this.getReconnectExponentialDelay()
2010 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2011 : this.getConnectionTimeout() * 1000;
2012 const reconnectDelayWithdraw = 1000;
2013 const reconnectTimeout =
2014 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2015 ? reconnectDelay - reconnectDelayWithdraw
2016 : 0;
2017 logger.error(
2018 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2019 reconnectDelay,
2020 2
2021 )}ms, timeout ${reconnectTimeout}ms`
2022 );
2023 await Utils.sleep(reconnectDelay);
2024 logger.error(
2025 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2026 );
2027 this.openWSConnection(
2028 {
2029 ...(this.stationInfo?.wsOptions ?? {}),
2030 handshakeTimeout: reconnectTimeout,
2031 },
2032 { closeOpened: true }
2033 );
2034 this.wsConnectionRestarted = true;
2035 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2036 logger.error(
2037 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2038 this.autoReconnectRetryCount
2039 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2040 );
2041 }
2042 }
2043
2044 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2045 | AutomaticTransactionGeneratorConfiguration
2046 | undefined {
2047 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
2048 }
2049
2050 private initializeConnectorStatus(connectorId: number): void {
2051 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2052 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2053 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
2054 this.getConnectorStatus(connectorId).transactionStarted = false;
2055 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2056 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
2057 }
2058 }