96bf6c1b3370fc29a772fc5baa01d5ef172c5509
[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 start(): void {
538 if (this.started === false) {
539 if (this.starting === false) {
540 this.starting = true;
541 if (this.getEnableStatistics() === true) {
542 this.performanceStatistics?.start();
543 }
544 this.openWSConnection();
545 // Monitor charging station template file
546 this.templateFileWatcher = FileUtils.watchJsonFile(
547 this.templateFile,
548 FileType.ChargingStationTemplate,
549 this.logPrefix(),
550 undefined,
551 (event, filename): void => {
552 if (Utils.isNotEmptyString(filename) && event === 'change') {
553 try {
554 logger.debug(
555 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
556 this.templateFile
557 } file have changed, reload`
558 );
559 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
560 // Initialize
561 this.initialize();
562 // Restart the ATG
563 this.stopAutomaticTransactionGenerator();
564 if (
565 this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true
566 ) {
567 this.startAutomaticTransactionGenerator();
568 }
569 if (this.getEnableStatistics() === true) {
570 this.performanceStatistics?.restart();
571 } else {
572 this.performanceStatistics?.stop();
573 }
574 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
575 } catch (error) {
576 logger.error(
577 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
578 error
579 );
580 }
581 }
582 }
583 );
584 this.started = true;
585 parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
586 this.starting = false;
587 } else {
588 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
589 }
590 } else {
591 logger.warn(`${this.logPrefix()} Charging station is already started...`);
592 }
593 }
594
595 public async stop(reason?: StopTransactionReason): Promise<void> {
596 if (this.started === true) {
597 if (this.stopping === false) {
598 this.stopping = true;
599 await this.stopMessageSequence(reason);
600 this.closeWSConnection();
601 if (this.getEnableStatistics() === true) {
602 this.performanceStatistics?.stop();
603 }
604 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
605 this.templateFileWatcher?.close();
606 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
607 delete this.bootNotificationResponse;
608 this.started = false;
609 parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
610 this.stopping = false;
611 } else {
612 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
613 }
614 } else {
615 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
616 }
617 }
618
619 public async reset(reason?: StopTransactionReason): Promise<void> {
620 await this.stop(reason);
621 await Utils.sleep(this.stationInfo.resetTime);
622 this.initialize();
623 this.start();
624 }
625
626 public saveOcppConfiguration(): void {
627 if (this.getOcppPersistentConfiguration()) {
628 this.saveConfiguration();
629 }
630 }
631
632 public resetConnectorStatus(connectorId: number): void {
633 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
634 this.getConnectorStatus(connectorId).idTagAuthorized = false;
635 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
636 this.getConnectorStatus(connectorId).transactionStarted = false;
637 delete this.getConnectorStatus(connectorId)?.localAuthorizeIdTag;
638 delete this.getConnectorStatus(connectorId)?.authorizeIdTag;
639 delete this.getConnectorStatus(connectorId)?.transactionId;
640 delete this.getConnectorStatus(connectorId)?.transactionIdTag;
641 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
642 delete this.getConnectorStatus(connectorId)?.transactionBeginMeterValue;
643 this.stopMeterValues(connectorId);
644 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
645 }
646
647 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
648 return ChargingStationConfigurationUtils.getConfigurationKey(
649 this,
650 StandardParametersKey.SupportedFeatureProfiles
651 )?.value?.includes(featureProfile);
652 }
653
654 public bufferMessage(message: string): void {
655 this.messageBuffer.add(message);
656 }
657
658 public openWSConnection(
659 options: WsOptions = this.stationInfo?.wsOptions ?? {},
660 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
661 closeOpened: false,
662 terminateOpened: false,
663 }
664 ): void {
665 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
666 params.closeOpened = params?.closeOpened ?? false;
667 params.terminateOpened = params?.terminateOpened ?? false;
668 if (this.started === false && this.starting === false) {
669 logger.warn(
670 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
671 );
672 return;
673 }
674 if (
675 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
676 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
677 ) {
678 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
679 }
680 if (params?.closeOpened) {
681 this.closeWSConnection();
682 }
683 if (params?.terminateOpened) {
684 this.terminateWSConnection();
685 }
686
687 if (this.isWebSocketConnectionOpened() === true) {
688 logger.warn(
689 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
690 );
691 return;
692 }
693
694 logger.info(
695 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
696 );
697
698 this.wsConnection = new WebSocket(
699 this.wsConnectionUrl,
700 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
701 options
702 );
703
704 // Handle WebSocket message
705 this.wsConnection.on(
706 'message',
707 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
708 );
709 // Handle WebSocket error
710 this.wsConnection.on(
711 'error',
712 this.onError.bind(this) as (this: WebSocket, error: Error) => void
713 );
714 // Handle WebSocket close
715 this.wsConnection.on(
716 'close',
717 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
718 );
719 // Handle WebSocket open
720 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
721 // Handle WebSocket ping
722 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
723 // Handle WebSocket pong
724 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
725 }
726
727 public closeWSConnection(): void {
728 if (this.isWebSocketConnectionOpened() === true) {
729 this.wsConnection?.close();
730 this.wsConnection = null;
731 }
732 }
733
734 public startAutomaticTransactionGenerator(
735 connectorIds?: number[],
736 automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
737 ): void {
738 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
739 automaticTransactionGeneratorConfiguration ??
740 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
741 this
742 );
743 if (Utils.isNotEmptyArray(connectorIds)) {
744 for (const connectorId of connectorIds) {
745 this.automaticTransactionGenerator?.startConnector(connectorId);
746 }
747 } else {
748 this.automaticTransactionGenerator?.start();
749 }
750 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
751 }
752
753 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
754 if (Utils.isNotEmptyArray(connectorIds)) {
755 for (const connectorId of connectorIds) {
756 this.automaticTransactionGenerator?.stopConnector(connectorId);
757 }
758 } else {
759 this.automaticTransactionGenerator?.stop();
760 }
761 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
762 }
763
764 public async stopTransactionOnConnector(
765 connectorId: number,
766 reason = StopTransactionReason.NONE
767 ): Promise<StopTransactionResponse> {
768 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
769 if (
770 this.getBeginEndMeterValues() === true &&
771 this.getOcppStrictCompliance() === true &&
772 this.getOutOfOrderEndMeterValues() === false
773 ) {
774 // FIXME: Implement OCPP version agnostic helpers
775 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
776 this,
777 connectorId,
778 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
779 );
780 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
781 this,
782 RequestCommand.METER_VALUES,
783 {
784 connectorId,
785 transactionId,
786 meterValue: [transactionEndMeterValue],
787 }
788 );
789 }
790 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
791 this,
792 RequestCommand.STOP_TRANSACTION,
793 {
794 transactionId,
795 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
796 reason,
797 }
798 );
799 }
800
801 private flushMessageBuffer(): void {
802 if (this.messageBuffer.size > 0) {
803 for (const message of this.messageBuffer.values()) {
804 let beginId: string;
805 let commandName: RequestCommand;
806 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
807 const isRequest = messageType === MessageType.CALL_MESSAGE;
808 if (isRequest) {
809 [, , commandName] = JSON.parse(message) as OutgoingRequest;
810 beginId = PerformanceStatistics.beginMeasure(commandName);
811 }
812 this.wsConnection?.send(message);
813 isRequest && PerformanceStatistics.endMeasure(commandName, beginId);
814 logger.debug(
815 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
816 messageType
817 )} payload sent: ${message}`
818 );
819 this.messageBuffer.delete(message);
820 }
821 }
822 }
823
824 private getSupervisionUrlOcppConfiguration(): boolean {
825 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
826 }
827
828 private getSupervisionUrlOcppKey(): string {
829 return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
830 }
831
832 private getTemplateFromFile(): ChargingStationTemplate | undefined {
833 let template: ChargingStationTemplate;
834 try {
835 if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) {
836 template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash);
837 } else {
838 const measureId = `${FileType.ChargingStationTemplate} read`;
839 const beginId = PerformanceStatistics.beginMeasure(measureId);
840 template = JSON.parse(
841 fs.readFileSync(this.templateFile, 'utf8')
842 ) as ChargingStationTemplate;
843 PerformanceStatistics.endMeasure(measureId, beginId);
844 template.templateHash = crypto
845 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
846 .update(JSON.stringify(template))
847 .digest('hex');
848 this.sharedLRUCache.setChargingStationTemplate(template);
849 }
850 } catch (error) {
851 FileUtils.handleFileException(
852 this.templateFile,
853 FileType.ChargingStationTemplate,
854 error as NodeJS.ErrnoException,
855 this.logPrefix()
856 );
857 }
858 return template;
859 }
860
861 private getStationInfoFromTemplate(): ChargingStationInfo {
862 const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
863 if (Utils.isNullOrUndefined(stationTemplate)) {
864 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
865 logger.error(`${this.logPrefix()} ${errorMsg}`);
866 throw new BaseError(errorMsg);
867 }
868 if (Utils.isEmptyObject(stationTemplate)) {
869 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
870 logger.error(`${this.logPrefix()} ${errorMsg}`);
871 throw new BaseError(errorMsg);
872 }
873 ChargingStationUtils.warnTemplateKeysDeprecation(
874 this.templateFile,
875 stationTemplate,
876 this.logPrefix()
877 );
878 const stationInfo: ChargingStationInfo =
879 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
880 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
881 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
882 this.index,
883 stationTemplate
884 );
885 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
886 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
887 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
888 stationTemplate.power = stationTemplate.power as number[];
889 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
890 stationInfo.maximumPower =
891 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
892 ? stationTemplate.power[powerArrayRandomIndex] * 1000
893 : stationTemplate.power[powerArrayRandomIndex];
894 } else {
895 stationTemplate.power = stationTemplate?.power as number;
896 stationInfo.maximumPower =
897 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
898 ? stationTemplate.power * 1000
899 : stationTemplate.power;
900 }
901 stationInfo.firmwareVersionPattern =
902 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
903 if (
904 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
905 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
906 ) {
907 logger.warn(
908 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
909 this.templateFile
910 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
911 );
912 }
913 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
914 {
915 versionUpgrade: {
916 step: 1,
917 },
918 reset: true,
919 },
920 stationTemplate?.firmwareUpgrade ?? {}
921 );
922 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
923 ? stationTemplate.resetTime * 1000
924 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
925 const configuredMaxConnectors =
926 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
927 ChargingStationUtils.checkConfiguredMaxConnectors(
928 configuredMaxConnectors,
929 this.templateFile,
930 this.logPrefix()
931 );
932 // Build evses or connectors if needed (FIXME: should be factored out)
933 if (stationInfo?.Connectors && !stationInfo?.Evses) {
934 const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
935 stationTemplate.Connectors
936 );
937 ChargingStationUtils.checkTemplateMaxConnectors(
938 templateMaxConnectors,
939 this.templateFile,
940 this.logPrefix()
941 );
942 if (
943 configuredMaxConnectors >
944 (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
945 !stationTemplate?.randomConnectors
946 ) {
947 logger.warn(
948 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
949 this.templateFile
950 }, forcing random connector configurations affectation`
951 );
952 stationInfo.randomConnectors = true;
953 }
954 this.initializeConnectors(stationInfo, configuredMaxConnectors);
955 } else if (stationInfo?.Evses && !stationInfo?.Connectors) {
956 this.initializeEvses(stationInfo);
957 } else if (stationInfo?.Evses && stationInfo?.Connectors) {
958 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
959 logger.error(`${this.logPrefix()} ${errorMsg}`);
960 throw new BaseError(errorMsg);
961 } else {
962 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
963 logger.error(`${this.logPrefix()} ${errorMsg}`);
964 throw new BaseError(errorMsg);
965 }
966 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
967 ChargingStationUtils.createStationInfoHash(stationInfo);
968 return stationInfo;
969 }
970
971 private getStationInfoFromFile(): ChargingStationInfo | undefined {
972 let stationInfo: ChargingStationInfo | undefined;
973 this.getStationInfoPersistentConfiguration() &&
974 (stationInfo = this.getConfigurationFromFile()?.stationInfo);
975 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
976 return stationInfo;
977 }
978
979 private getStationInfo(): ChargingStationInfo {
980 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
981 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
982 // Priority:
983 // 1. charging station info from template
984 // 2. charging station info from configuration file
985 // 3. 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 ): void {
1265 if (!stationInfo?.Connectors && this.connectors.size === 0) {
1266 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1267 logger.error(`${this.logPrefix()} ${logMsg}`);
1268 throw new BaseError(logMsg);
1269 }
1270 if (!stationInfo?.Connectors[0]) {
1271 logger.warn(
1272 `${this.logPrefix()} Charging station information from template ${
1273 this.templateFile
1274 } with no connector id 0 configuration`
1275 );
1276 }
1277 if (stationInfo?.Connectors) {
1278 const connectorsConfigHash = crypto
1279 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1280 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
1281 .digest('hex');
1282 const connectorsConfigChanged =
1283 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1284 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1285 connectorsConfigChanged && this.connectors.clear();
1286 this.connectorsConfigurationHash = connectorsConfigHash;
1287 // Add connector id 0
1288 let lastConnector = '0';
1289 for (lastConnector in stationInfo?.Connectors) {
1290 const connectorStatus = stationInfo?.Connectors[lastConnector];
1291 const lastConnectorId = Utils.convertToInt(lastConnector);
1292 if (
1293 lastConnectorId === 0 &&
1294 this.getUseConnectorId0(stationInfo) === true &&
1295 connectorStatus
1296 ) {
1297 this.checkStationInfoConnectorStatus(lastConnectorId, connectorStatus);
1298 this.connectors.set(
1299 lastConnectorId,
1300 Utils.cloneObject<ConnectorStatus>(connectorStatus)
1301 );
1302 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.Operative;
1303 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1304 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1305 }
1306 }
1307 }
1308 // Generate all connectors
1309 const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
1310 stationInfo?.Connectors
1311 );
1312 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
1313 for (let index = 1; index <= configuredMaxConnectors; index++) {
1314 const randConnectorId = stationInfo?.randomConnectors
1315 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1316 : index;
1317 const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
1318 this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
1319 this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1320 this.getConnectorStatus(index).availability = AvailabilityType.Operative;
1321 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1322 this.getConnectorStatus(index).chargingProfiles = [];
1323 }
1324 }
1325 }
1326 }
1327 } else {
1328 logger.warn(
1329 `${this.logPrefix()} Charging station information from template ${
1330 this.templateFile
1331 } with no connectors configuration defined, using already defined connectors`
1332 );
1333 }
1334 // Initialize connectors status
1335 for (const connectorId of this.connectors.keys()) {
1336 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1337 logger.warn(
1338 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
1339 this.getConnectorStatus(connectorId)?.transactionId
1340 }`
1341 );
1342 }
1343 if (
1344 connectorId > 0 &&
1345 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionStarted)
1346 ) {
1347 this.initializeConnectorStatus(this.getConnectorStatus(connectorId));
1348 }
1349 }
1350 }
1351
1352 private buildConnectorsMap(
1353 connectors: Record<string, ConnectorStatus>
1354 ): Map<number, ConnectorStatus> {
1355 const connectorsMap = new Map<number, ConnectorStatus>();
1356 for (const connector in connectors) {
1357 const connectorStatus = connectors[connector];
1358 const connectorId = Utils.convertToInt(connector);
1359 this.checkStationInfoConnectorStatus(connectorId, connectorStatus);
1360 connectorsMap.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1361 connectorsMap.get(connectorId).availability = AvailabilityType.Operative;
1362 if (Utils.isUndefined(connectorsMap.get(connectorId)?.chargingProfiles)) {
1363 connectorsMap.get(connectorId).chargingProfiles = [];
1364 }
1365 }
1366 return connectorsMap;
1367 }
1368
1369 private initializeConnectorsMapStatus(connectors: Map<number, ConnectorStatus>): void {
1370 for (const connectorId of connectors.keys()) {
1371 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
1372 logger.warn(
1373 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
1374 connectors.get(connectorId)?.transactionId
1375 }`
1376 );
1377 }
1378 if (
1379 connectorId > 0 &&
1380 Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
1381 ) {
1382 this.initializeConnectorStatus(connectors.get(connectorId));
1383 }
1384 }
1385 }
1386
1387 private initializeEvses(stationInfo: ChargingStationInfo): void {
1388 if (!stationInfo?.Evses && this.evses.size === 0) {
1389 const logMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1390 logger.warn(`${this.logPrefix()} ${logMsg}`);
1391 return;
1392 }
1393 if (!stationInfo?.Evses[0]) {
1394 logger.warn(
1395 `${this.logPrefix()} Charging station information from template ${
1396 this.templateFile
1397 } with no evse id 0 configuration`
1398 );
1399 }
1400 if (stationInfo?.Evses) {
1401 const evsesConfigHash = crypto
1402 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1403 .update(`${JSON.stringify(stationInfo?.Evses)}`)
1404 .digest('hex');
1405 const evsesConfigChanged =
1406 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1407 if (this.evses?.size === 0 || evsesConfigChanged) {
1408 evsesConfigChanged && this.evses.clear();
1409 this.evsesConfigurationHash = evsesConfigHash;
1410 for (const evse in stationInfo?.Evses) {
1411 this.evses.set(Utils.convertToInt(evse), {
1412 connectors: this.buildConnectorsMap(stationInfo?.Evses[evse]?.Connectors),
1413 availability: AvailabilityType.Operative,
1414 });
1415 this.initializeConnectorsMapStatus(this.evses.get(Utils.convertToInt(evse))?.connectors);
1416 }
1417 }
1418 } else {
1419 logger.warn(
1420 `${this.logPrefix()} Charging station information from template ${
1421 this.templateFile
1422 } with no evses configuration defined, using already defined evses`
1423 );
1424 }
1425 }
1426
1427 private checkStationInfoConnectorStatus(
1428 connectorId: number,
1429 connectorStatus: ConnectorStatus
1430 ): void {
1431 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1432 logger.warn(
1433 `${this.logPrefix()} Charging station information from template ${
1434 this.templateFile
1435 } with connector ${connectorId} status configuration defined, undefine it`
1436 );
1437 delete connectorStatus.status;
1438 }
1439 }
1440
1441 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1442 let configuration: ChargingStationConfiguration | undefined;
1443 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1444 try {
1445 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1446 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1447 this.configurationFileHash
1448 );
1449 } else {
1450 const measureId = `${FileType.ChargingStationConfiguration} read`;
1451 const beginId = PerformanceStatistics.beginMeasure(measureId);
1452 configuration = JSON.parse(
1453 fs.readFileSync(this.configurationFile, 'utf8')
1454 ) as ChargingStationConfiguration;
1455 PerformanceStatistics.endMeasure(measureId, beginId);
1456 this.configurationFileHash = configuration.configurationHash;
1457 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1458 }
1459 } catch (error) {
1460 FileUtils.handleFileException(
1461 this.configurationFile,
1462 FileType.ChargingStationConfiguration,
1463 error as NodeJS.ErrnoException,
1464 this.logPrefix()
1465 );
1466 }
1467 }
1468 return configuration;
1469 }
1470
1471 private saveConfiguration(): void {
1472 if (this.configurationFile) {
1473 try {
1474 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1475 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1476 }
1477 const configurationData: ChargingStationConfiguration =
1478 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
1479 this.ocppConfiguration?.configurationKey &&
1480 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1481 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1482 delete configurationData.configurationHash;
1483 const configurationHash = crypto
1484 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1485 .update(JSON.stringify(configurationData))
1486 .digest('hex');
1487 if (this.configurationFileHash !== configurationHash) {
1488 configurationData.configurationHash = configurationHash;
1489 const measureId = `${FileType.ChargingStationConfiguration} write`;
1490 const beginId = PerformanceStatistics.beginMeasure(measureId);
1491 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1492 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1493 fs.closeSync(fileDescriptor);
1494 PerformanceStatistics.endMeasure(measureId, beginId);
1495 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1496 this.configurationFileHash = configurationHash;
1497 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1498 } else {
1499 logger.debug(
1500 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1501 this.configurationFile
1502 }`
1503 );
1504 }
1505 } catch (error) {
1506 FileUtils.handleFileException(
1507 this.configurationFile,
1508 FileType.ChargingStationConfiguration,
1509 error as NodeJS.ErrnoException,
1510 this.logPrefix()
1511 );
1512 }
1513 } else {
1514 logger.error(
1515 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1516 );
1517 }
1518 }
1519
1520 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1521 return this.getTemplateFromFile()?.Configuration;
1522 }
1523
1524 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1525 let configuration: ChargingStationConfiguration | undefined;
1526 if (this.getOcppPersistentConfiguration() === true) {
1527 const configurationFromFile = this.getConfigurationFromFile();
1528 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1529 }
1530 if (!Utils.isNullOrUndefined(configuration)) {
1531 delete configuration.stationInfo;
1532 delete configuration.configurationHash;
1533 }
1534 return configuration;
1535 }
1536
1537 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1538 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1539 this.getOcppConfigurationFromFile();
1540 if (!ocppConfiguration) {
1541 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1542 }
1543 return ocppConfiguration;
1544 }
1545
1546 private async onOpen(): Promise<void> {
1547 if (this.isWebSocketConnectionOpened() === true) {
1548 logger.info(
1549 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1550 );
1551 if (this.isRegistered() === false) {
1552 // Send BootNotification
1553 let registrationRetryCount = 0;
1554 do {
1555 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1556 BootNotificationRequest,
1557 BootNotificationResponse
1558 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1559 skipBufferingOnError: true,
1560 });
1561 if (this.isRegistered() === false) {
1562 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1563 await Utils.sleep(
1564 this?.bootNotificationResponse?.interval
1565 ? this.bootNotificationResponse.interval * 1000
1566 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1567 );
1568 }
1569 } while (
1570 this.isRegistered() === false &&
1571 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1572 this.getRegistrationMaxRetries() === -1)
1573 );
1574 }
1575 if (this.isRegistered() === true) {
1576 if (this.isInAcceptedState() === true) {
1577 await this.startMessageSequence();
1578 }
1579 } else {
1580 logger.error(
1581 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1582 );
1583 }
1584 this.wsConnectionRestarted = false;
1585 this.autoReconnectRetryCount = 0;
1586 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1587 } else {
1588 logger.warn(
1589 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1590 );
1591 }
1592 }
1593
1594 private async onClose(code: number, reason: Buffer): Promise<void> {
1595 switch (code) {
1596 // Normal close
1597 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1598 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1599 logger.info(
1600 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1601 code
1602 )}' and reason '${reason.toString()}'`
1603 );
1604 this.autoReconnectRetryCount = 0;
1605 break;
1606 // Abnormal close
1607 default:
1608 logger.error(
1609 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1610 code
1611 )}' and reason '${reason.toString()}'`
1612 );
1613 this.started === true && (await this.reconnect());
1614 break;
1615 }
1616 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1617 }
1618
1619 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1620 const cachedRequest = this.requests.get(messageId);
1621 if (Array.isArray(cachedRequest) === true) {
1622 return cachedRequest;
1623 }
1624 throw new OCPPError(
1625 ErrorType.PROTOCOL_ERROR,
1626 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1627 messageType
1628 )} is not an array`,
1629 undefined,
1630 cachedRequest as JsonType
1631 );
1632 }
1633
1634 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1635 const [messageType, messageId, commandName, commandPayload] = request;
1636 if (this.getEnableStatistics() === true) {
1637 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1638 }
1639 logger.debug(
1640 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1641 request
1642 )}`
1643 );
1644 // Process the message
1645 await this.ocppIncomingRequestService.incomingRequestHandler(
1646 this,
1647 messageId,
1648 commandName,
1649 commandPayload
1650 );
1651 }
1652
1653 private handleResponseMessage(response: Response): void {
1654 const [messageType, messageId, commandPayload] = response;
1655 if (this.requests.has(messageId) === false) {
1656 // Error
1657 throw new OCPPError(
1658 ErrorType.INTERNAL_ERROR,
1659 `Response for unknown message id ${messageId}`,
1660 undefined,
1661 commandPayload
1662 );
1663 }
1664 // Respond
1665 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1666 messageType,
1667 messageId
1668 );
1669 logger.debug(
1670 `${this.logPrefix()} << Command '${
1671 requestCommandName ?? Constants.UNKNOWN_COMMAND
1672 }' received response payload: ${JSON.stringify(response)}`
1673 );
1674 responseCallback(commandPayload, requestPayload);
1675 }
1676
1677 private handleErrorMessage(errorResponse: ErrorResponse): void {
1678 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1679 if (this.requests.has(messageId) === false) {
1680 // Error
1681 throw new OCPPError(
1682 ErrorType.INTERNAL_ERROR,
1683 `Error response for unknown message id ${messageId}`,
1684 undefined,
1685 { errorType, errorMessage, errorDetails }
1686 );
1687 }
1688 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1689 logger.debug(
1690 `${this.logPrefix()} << Command '${
1691 requestCommandName ?? Constants.UNKNOWN_COMMAND
1692 }' received error response payload: ${JSON.stringify(errorResponse)}`
1693 );
1694 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1695 }
1696
1697 private async onMessage(data: RawData): Promise<void> {
1698 let request: IncomingRequest | Response | ErrorResponse;
1699 let messageType: number;
1700 let errMsg: string;
1701 try {
1702 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1703 if (Array.isArray(request) === true) {
1704 [messageType] = request;
1705 // Check the type of message
1706 switch (messageType) {
1707 // Incoming Message
1708 case MessageType.CALL_MESSAGE:
1709 await this.handleIncomingMessage(request as IncomingRequest);
1710 break;
1711 // Response Message
1712 case MessageType.CALL_RESULT_MESSAGE:
1713 this.handleResponseMessage(request as Response);
1714 break;
1715 // Error Message
1716 case MessageType.CALL_ERROR_MESSAGE:
1717 this.handleErrorMessage(request as ErrorResponse);
1718 break;
1719 // Unknown Message
1720 default:
1721 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1722 errMsg = `Wrong message type ${messageType}`;
1723 logger.error(`${this.logPrefix()} ${errMsg}`);
1724 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1725 }
1726 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1727 } else {
1728 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1729 request,
1730 });
1731 }
1732 } catch (error) {
1733 let commandName: IncomingRequestCommand;
1734 let requestCommandName: RequestCommand | IncomingRequestCommand;
1735 let errorCallback: ErrorCallback;
1736 const [, messageId] = request;
1737 switch (messageType) {
1738 case MessageType.CALL_MESSAGE:
1739 [, , commandName] = request as IncomingRequest;
1740 // Send error
1741 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1742 break;
1743 case MessageType.CALL_RESULT_MESSAGE:
1744 case MessageType.CALL_ERROR_MESSAGE:
1745 if (this.requests.has(messageId) === true) {
1746 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1747 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1748 errorCallback(error as OCPPError, false);
1749 } else {
1750 // Remove the request from the cache in case of error at response handling
1751 this.requests.delete(messageId);
1752 }
1753 break;
1754 }
1755 if (error instanceof OCPPError === false) {
1756 logger.warn(
1757 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1758 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1759 }' message '${data.toString()}' handling is not an OCPPError:`,
1760 error
1761 );
1762 }
1763 logger.error(
1764 `${this.logPrefix()} Incoming OCPP command '${
1765 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1766 }' message '${data.toString()}'${
1767 messageType !== MessageType.CALL_MESSAGE
1768 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1769 : ''
1770 } processing error:`,
1771 error
1772 );
1773 }
1774 }
1775
1776 private onPing(): void {
1777 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1778 }
1779
1780 private onPong(): void {
1781 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1782 }
1783
1784 private onError(error: WSError): void {
1785 this.closeWSConnection();
1786 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1787 }
1788
1789 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1790 if (this.getMeteringPerTransaction() === true) {
1791 return (
1792 (rounded === true
1793 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1794 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1795 );
1796 }
1797 return (
1798 (rounded === true
1799 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1800 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1801 );
1802 }
1803
1804 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1805 const localStationInfo = stationInfo ?? this.stationInfo;
1806 return localStationInfo?.useConnectorId0 ?? true;
1807 }
1808
1809 private getNumberOfRunningTransactions(): number {
1810 let trxCount = 0;
1811 for (const connectorId of this.connectors.keys()) {
1812 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1813 trxCount++;
1814 }
1815 }
1816 return trxCount;
1817 }
1818
1819 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1820 for (const connectorId of this.connectors.keys()) {
1821 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1822 await this.stopTransactionOnConnector(connectorId, reason);
1823 }
1824 }
1825 }
1826
1827 // 0 for disabling
1828 private getConnectionTimeout(): number {
1829 if (
1830 ChargingStationConfigurationUtils.getConfigurationKey(
1831 this,
1832 StandardParametersKey.ConnectionTimeOut
1833 )
1834 ) {
1835 return (
1836 parseInt(
1837 ChargingStationConfigurationUtils.getConfigurationKey(
1838 this,
1839 StandardParametersKey.ConnectionTimeOut
1840 ).value
1841 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1842 );
1843 }
1844 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1845 }
1846
1847 // -1 for unlimited, 0 for disabling
1848 private getAutoReconnectMaxRetries(): number | undefined {
1849 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1850 return this.stationInfo.autoReconnectMaxRetries;
1851 }
1852 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1853 return Configuration.getAutoReconnectMaxRetries();
1854 }
1855 return -1;
1856 }
1857
1858 // 0 for disabling
1859 private getRegistrationMaxRetries(): number | undefined {
1860 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1861 return this.stationInfo.registrationMaxRetries;
1862 }
1863 return -1;
1864 }
1865
1866 private getPowerDivider(): number {
1867 let powerDivider = this.getNumberOfConnectors();
1868 if (this.stationInfo?.powerSharedByConnectors) {
1869 powerDivider = this.getNumberOfRunningTransactions();
1870 }
1871 return powerDivider;
1872 }
1873
1874 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1875 const maximumPower = this.getMaximumPower(stationInfo);
1876 switch (this.getCurrentOutType(stationInfo)) {
1877 case CurrentType.AC:
1878 return ACElectricUtils.amperagePerPhaseFromPower(
1879 this.getNumberOfPhases(stationInfo),
1880 maximumPower / this.getNumberOfConnectors(),
1881 this.getVoltageOut(stationInfo)
1882 );
1883 case CurrentType.DC:
1884 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1885 }
1886 }
1887
1888 private getAmperageLimitation(): number | undefined {
1889 if (
1890 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1891 ChargingStationConfigurationUtils.getConfigurationKey(
1892 this,
1893 this.stationInfo.amperageLimitationOcppKey
1894 )
1895 ) {
1896 return (
1897 Utils.convertToInt(
1898 ChargingStationConfigurationUtils.getConfigurationKey(
1899 this,
1900 this.stationInfo.amperageLimitationOcppKey
1901 )?.value
1902 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1903 );
1904 }
1905 }
1906
1907 private async startMessageSequence(): Promise<void> {
1908 if (this.stationInfo?.autoRegister === true) {
1909 await this.ocppRequestService.requestHandler<
1910 BootNotificationRequest,
1911 BootNotificationResponse
1912 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1913 skipBufferingOnError: true,
1914 });
1915 }
1916 // Start WebSocket ping
1917 this.startWebSocketPing();
1918 // Start heartbeat
1919 this.startHeartbeat();
1920 // Initialize connectors status
1921 for (const connectorId of this.connectors.keys()) {
1922 let connectorStatus: ConnectorStatusEnum | undefined;
1923 if (connectorId === 0) {
1924 continue;
1925 } else if (
1926 !this.getConnectorStatus(connectorId)?.status &&
1927 (this.isChargingStationAvailable() === false ||
1928 this.isConnectorAvailable(connectorId) === false)
1929 ) {
1930 connectorStatus = ConnectorStatusEnum.Unavailable;
1931 } else if (
1932 !this.getConnectorStatus(connectorId)?.status &&
1933 this.getConnectorStatus(connectorId)?.bootStatus
1934 ) {
1935 // Set boot status in template at startup
1936 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
1937 } else if (this.getConnectorStatus(connectorId)?.status) {
1938 // Set previous status at startup
1939 connectorStatus = this.getConnectorStatus(connectorId)?.status;
1940 } else {
1941 // Set default status
1942 connectorStatus = ConnectorStatusEnum.Available;
1943 }
1944 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorStatus);
1945 }
1946 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1947 await this.ocppRequestService.requestHandler<
1948 FirmwareStatusNotificationRequest,
1949 FirmwareStatusNotificationResponse
1950 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1951 status: FirmwareStatus.Installed,
1952 });
1953 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
1954 }
1955
1956 // Start the ATG
1957 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
1958 this.startAutomaticTransactionGenerator();
1959 }
1960 this.wsConnectionRestarted === true && this.flushMessageBuffer();
1961 }
1962
1963 private async stopMessageSequence(
1964 reason: StopTransactionReason = StopTransactionReason.NONE
1965 ): Promise<void> {
1966 // Stop WebSocket ping
1967 this.stopWebSocketPing();
1968 // Stop heartbeat
1969 this.stopHeartbeat();
1970 // Stop ongoing transactions
1971 if (this.automaticTransactionGenerator?.started === true) {
1972 this.stopAutomaticTransactionGenerator();
1973 } else {
1974 await this.stopRunningTransactions(reason);
1975 }
1976 for (const connectorId of this.connectors.keys()) {
1977 if (connectorId > 0) {
1978 await this.ocppRequestService.requestHandler<
1979 StatusNotificationRequest,
1980 StatusNotificationResponse
1981 >(
1982 this,
1983 RequestCommand.STATUS_NOTIFICATION,
1984 OCPPServiceUtils.buildStatusNotificationRequest(
1985 this,
1986 connectorId,
1987 ConnectorStatusEnum.Unavailable
1988 )
1989 );
1990 delete this.getConnectorStatus(connectorId)?.status;
1991 }
1992 }
1993 }
1994
1995 private startWebSocketPing(): void {
1996 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1997 this,
1998 StandardParametersKey.WebSocketPingInterval
1999 )
2000 ? Utils.convertToInt(
2001 ChargingStationConfigurationUtils.getConfigurationKey(
2002 this,
2003 StandardParametersKey.WebSocketPingInterval
2004 )?.value
2005 )
2006 : 0;
2007 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2008 this.webSocketPingSetInterval = setInterval(() => {
2009 if (this.isWebSocketConnectionOpened() === true) {
2010 this.wsConnection?.ping();
2011 }
2012 }, webSocketPingInterval * 1000);
2013 logger.info(
2014 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2015 webSocketPingInterval
2016 )}`
2017 );
2018 } else if (this.webSocketPingSetInterval) {
2019 logger.info(
2020 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2021 webSocketPingInterval
2022 )}`
2023 );
2024 } else {
2025 logger.error(
2026 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2027 );
2028 }
2029 }
2030
2031 private stopWebSocketPing(): void {
2032 if (this.webSocketPingSetInterval) {
2033 clearInterval(this.webSocketPingSetInterval);
2034 delete this.webSocketPingSetInterval;
2035 }
2036 }
2037
2038 private getConfiguredSupervisionUrl(): URL {
2039 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2040 if (Utils.isNotEmptyArray(supervisionUrls)) {
2041 let configuredSupervisionUrlIndex: number;
2042 switch (Configuration.getSupervisionUrlDistribution()) {
2043 case SupervisionUrlDistribution.RANDOM:
2044 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2045 break;
2046 case SupervisionUrlDistribution.ROUND_ROBIN:
2047 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2048 default:
2049 Object.values(SupervisionUrlDistribution).includes(
2050 Configuration.getSupervisionUrlDistribution()
2051 ) === false &&
2052 logger.error(
2053 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2054 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2055 }`
2056 );
2057 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2058 break;
2059 }
2060 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
2061 }
2062 return new URL(supervisionUrls as string);
2063 }
2064
2065 private stopHeartbeat(): void {
2066 if (this.heartbeatSetInterval) {
2067 clearInterval(this.heartbeatSetInterval);
2068 delete this.heartbeatSetInterval;
2069 }
2070 }
2071
2072 private terminateWSConnection(): void {
2073 if (this.isWebSocketConnectionOpened() === true) {
2074 this.wsConnection?.terminate();
2075 this.wsConnection = null;
2076 }
2077 }
2078
2079 private stopMeterValues(connectorId: number) {
2080 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
2081 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
2082 }
2083 }
2084
2085 private getReconnectExponentialDelay(): boolean {
2086 return this.stationInfo?.reconnectExponentialDelay ?? false;
2087 }
2088
2089 private async reconnect(): Promise<void> {
2090 // Stop WebSocket ping
2091 this.stopWebSocketPing();
2092 // Stop heartbeat
2093 this.stopHeartbeat();
2094 // Stop the ATG if needed
2095 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2096 this.stopAutomaticTransactionGenerator();
2097 }
2098 if (
2099 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2100 this.getAutoReconnectMaxRetries() === -1
2101 ) {
2102 this.autoReconnectRetryCount++;
2103 const reconnectDelay = this.getReconnectExponentialDelay()
2104 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2105 : this.getConnectionTimeout() * 1000;
2106 const reconnectDelayWithdraw = 1000;
2107 const reconnectTimeout =
2108 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2109 ? reconnectDelay - reconnectDelayWithdraw
2110 : 0;
2111 logger.error(
2112 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2113 reconnectDelay,
2114 2
2115 )}ms, timeout ${reconnectTimeout}ms`
2116 );
2117 await Utils.sleep(reconnectDelay);
2118 logger.error(
2119 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2120 );
2121 this.openWSConnection(
2122 {
2123 ...(this.stationInfo?.wsOptions ?? {}),
2124 handshakeTimeout: reconnectTimeout,
2125 },
2126 { closeOpened: true }
2127 );
2128 this.wsConnectionRestarted = true;
2129 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2130 logger.error(
2131 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2132 this.autoReconnectRetryCount
2133 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2134 );
2135 }
2136 }
2137
2138 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2139 | AutomaticTransactionGeneratorConfiguration
2140 | undefined {
2141 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
2142 }
2143
2144 private initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
2145 connectorStatus.idTagLocalAuthorized = false;
2146 connectorStatus.idTagAuthorized = false;
2147 connectorStatus.transactionRemoteStarted = false;
2148 connectorStatus.transactionStarted = false;
2149 connectorStatus.energyActiveImportRegisterValue = 0;
2150 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
2151 }
2152 }