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