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