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