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