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