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