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