fix: fix CS configuration hash calculation
[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 // FIXME: template changes to evses or connectors are not taken into account
1098 if (
1099 chargingStationConfiguration?.connectorsStatus ||
1100 chargingStationConfiguration?.evsesStatus
1101 ) {
1102 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
1103 } else {
1104 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
1105 }
1106 this.stationInfo = this.getStationInfo();
1107 if (
1108 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1109 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1110 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
1111 ) {
1112 const patternGroup: number | undefined =
1113 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1114 this.stationInfo.firmwareVersion?.split('.').length;
1115 const match = this.stationInfo?.firmwareVersion
1116 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1117 ?.slice(1, patternGroup + 1);
1118 const patchLevelIndex = match.length - 1;
1119 match[patchLevelIndex] = (
1120 Utils.convertToInt(match[patchLevelIndex]) +
1121 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
1122 ).toString();
1123 this.stationInfo.firmwareVersion = match?.join('.');
1124 }
1125 this.saveStationInfo();
1126 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1127 if (this.getEnableStatistics() === true) {
1128 this.performanceStatistics = PerformanceStatistics.getInstance(
1129 this.stationInfo.hashId,
1130 this.stationInfo.chargingStationId,
1131 this.configuredSupervisionUrl
1132 );
1133 }
1134 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1135 this.stationInfo
1136 );
1137 this.powerDivider = this.getPowerDivider();
1138 // OCPP configuration
1139 this.ocppConfiguration = this.getOcppConfiguration();
1140 this.initializeOcppConfiguration();
1141 this.initializeOcppServices();
1142 if (this.stationInfo?.autoRegister === true) {
1143 this.bootNotificationResponse = {
1144 currentTime: new Date(),
1145 interval: this.getHeartbeatInterval() / 1000,
1146 status: RegistrationStatusEnumType.ACCEPTED,
1147 };
1148 }
1149 }
1150
1151 private initializeOcppServices(): void {
1152 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1153 switch (ocppVersion) {
1154 case OCPPVersion.VERSION_16:
1155 this.ocppIncomingRequestService =
1156 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1157 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1158 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1159 );
1160 break;
1161 case OCPPVersion.VERSION_20:
1162 case OCPPVersion.VERSION_201:
1163 this.ocppIncomingRequestService =
1164 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1165 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1166 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1167 );
1168 break;
1169 default:
1170 this.handleUnsupportedVersion(ocppVersion);
1171 break;
1172 }
1173 }
1174
1175 private initializeOcppConfiguration(): void {
1176 if (
1177 !ChargingStationConfigurationUtils.getConfigurationKey(
1178 this,
1179 StandardParametersKey.HeartbeatInterval
1180 )
1181 ) {
1182 ChargingStationConfigurationUtils.addConfigurationKey(
1183 this,
1184 StandardParametersKey.HeartbeatInterval,
1185 '0'
1186 );
1187 }
1188 if (
1189 !ChargingStationConfigurationUtils.getConfigurationKey(
1190 this,
1191 StandardParametersKey.HeartBeatInterval
1192 )
1193 ) {
1194 ChargingStationConfigurationUtils.addConfigurationKey(
1195 this,
1196 StandardParametersKey.HeartBeatInterval,
1197 '0',
1198 { visible: false }
1199 );
1200 }
1201 if (
1202 this.getSupervisionUrlOcppConfiguration() &&
1203 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1204 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1205 ) {
1206 ChargingStationConfigurationUtils.addConfigurationKey(
1207 this,
1208 this.getSupervisionUrlOcppKey(),
1209 this.configuredSupervisionUrl.href,
1210 { reboot: true }
1211 );
1212 } else if (
1213 !this.getSupervisionUrlOcppConfiguration() &&
1214 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1215 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1216 ) {
1217 ChargingStationConfigurationUtils.deleteConfigurationKey(
1218 this,
1219 this.getSupervisionUrlOcppKey(),
1220 { save: false }
1221 );
1222 }
1223 if (
1224 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1225 !ChargingStationConfigurationUtils.getConfigurationKey(
1226 this,
1227 this.stationInfo.amperageLimitationOcppKey
1228 )
1229 ) {
1230 ChargingStationConfigurationUtils.addConfigurationKey(
1231 this,
1232 this.stationInfo.amperageLimitationOcppKey,
1233 (
1234 this.stationInfo.maximumAmperage *
1235 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1236 ).toString()
1237 );
1238 }
1239 if (
1240 !ChargingStationConfigurationUtils.getConfigurationKey(
1241 this,
1242 StandardParametersKey.SupportedFeatureProfiles
1243 )
1244 ) {
1245 ChargingStationConfigurationUtils.addConfigurationKey(
1246 this,
1247 StandardParametersKey.SupportedFeatureProfiles,
1248 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1249 );
1250 }
1251 ChargingStationConfigurationUtils.addConfigurationKey(
1252 this,
1253 StandardParametersKey.NumberOfConnectors,
1254 this.getNumberOfConnectors().toString(),
1255 { readonly: true },
1256 { overwrite: true }
1257 );
1258 if (
1259 !ChargingStationConfigurationUtils.getConfigurationKey(
1260 this,
1261 StandardParametersKey.MeterValuesSampledData
1262 )
1263 ) {
1264 ChargingStationConfigurationUtils.addConfigurationKey(
1265 this,
1266 StandardParametersKey.MeterValuesSampledData,
1267 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1268 );
1269 }
1270 if (
1271 !ChargingStationConfigurationUtils.getConfigurationKey(
1272 this,
1273 StandardParametersKey.ConnectorPhaseRotation
1274 )
1275 ) {
1276 const connectorsPhaseRotation: string[] = [];
1277 if (this.hasEvses) {
1278 for (const evseStatus of this.evses.values()) {
1279 for (const connectorId of evseStatus.connectors.keys()) {
1280 connectorsPhaseRotation.push(
1281 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1282 );
1283 }
1284 }
1285 } else {
1286 for (const connectorId of this.connectors.keys()) {
1287 connectorsPhaseRotation.push(
1288 ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
1289 );
1290 }
1291 }
1292 ChargingStationConfigurationUtils.addConfigurationKey(
1293 this,
1294 StandardParametersKey.ConnectorPhaseRotation,
1295 connectorsPhaseRotation.toString()
1296 );
1297 }
1298 if (
1299 !ChargingStationConfigurationUtils.getConfigurationKey(
1300 this,
1301 StandardParametersKey.AuthorizeRemoteTxRequests
1302 )
1303 ) {
1304 ChargingStationConfigurationUtils.addConfigurationKey(
1305 this,
1306 StandardParametersKey.AuthorizeRemoteTxRequests,
1307 'true'
1308 );
1309 }
1310 if (
1311 !ChargingStationConfigurationUtils.getConfigurationKey(
1312 this,
1313 StandardParametersKey.LocalAuthListEnabled
1314 ) &&
1315 ChargingStationConfigurationUtils.getConfigurationKey(
1316 this,
1317 StandardParametersKey.SupportedFeatureProfiles
1318 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1319 ) {
1320 ChargingStationConfigurationUtils.addConfigurationKey(
1321 this,
1322 StandardParametersKey.LocalAuthListEnabled,
1323 'false'
1324 );
1325 }
1326 if (
1327 !ChargingStationConfigurationUtils.getConfigurationKey(
1328 this,
1329 StandardParametersKey.ConnectionTimeOut
1330 )
1331 ) {
1332 ChargingStationConfigurationUtils.addConfigurationKey(
1333 this,
1334 StandardParametersKey.ConnectionTimeOut,
1335 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1336 );
1337 }
1338 this.saveOcppConfiguration();
1339 }
1340
1341 private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
1342 if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
1343 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1344 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1345 }
1346 } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
1347 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1348 const evseStatus = Utils.cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
1349 delete evseStatus.connectorsStatus;
1350 this.evses.set(evseId, {
1351 ...(evseStatus as EvseStatus),
1352 connectors: new Map<number, ConnectorStatus>(
1353 evseStatusConfiguration.connectorsStatus.map((connectorStatus, connectorId) => [
1354 connectorId,
1355 connectorStatus,
1356 ])
1357 ),
1358 });
1359 }
1360 } else if (configuration?.evsesStatus && configuration?.connectorsStatus) {
1361 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1362 logger.error(`${this.logPrefix()} ${errorMsg}`);
1363 throw new BaseError(errorMsg);
1364 } else {
1365 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`;
1366 logger.error(`${this.logPrefix()} ${errorMsg}`);
1367 throw new BaseError(errorMsg);
1368 }
1369 }
1370
1371 private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) {
1372 if (stationTemplate?.Connectors && !stationTemplate?.Evses) {
1373 this.initializeConnectorsFromTemplate(stationTemplate);
1374 } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) {
1375 this.initializeEvsesFromTemplate(stationTemplate);
1376 } else if (stationTemplate?.Evses && stationTemplate?.Connectors) {
1377 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1378 logger.error(`${this.logPrefix()} ${errorMsg}`);
1379 throw new BaseError(errorMsg);
1380 } else {
1381 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
1382 logger.error(`${this.logPrefix()} ${errorMsg}`);
1383 throw new BaseError(errorMsg);
1384 }
1385 }
1386
1387 private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void {
1388 if (!stationTemplate?.Connectors && this.connectors.size === 0) {
1389 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1390 logger.error(`${this.logPrefix()} ${errorMsg}`);
1391 throw new BaseError(errorMsg);
1392 }
1393 if (!stationTemplate?.Connectors[0]) {
1394 logger.warn(
1395 `${this.logPrefix()} Charging station information from template ${
1396 this.templateFile
1397 } with no connector id 0 configuration`
1398 );
1399 }
1400 if (stationTemplate?.Connectors) {
1401 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1402 ChargingStationUtils.checkConnectorsConfiguration(
1403 stationTemplate,
1404 this.logPrefix(),
1405 this.templateFile
1406 );
1407 const connectorsConfigHash = crypto
1408 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1409 .update(
1410 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
1411 )
1412 .digest('hex');
1413 const connectorsConfigChanged =
1414 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1415 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1416 connectorsConfigChanged && this.connectors.clear();
1417 this.connectorsConfigurationHash = connectorsConfigHash;
1418 if (templateMaxConnectors > 0) {
1419 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1420 if (
1421 connectorId === 0 &&
1422 (!stationTemplate?.Connectors[connectorId] ||
1423 this.getUseConnectorId0(stationTemplate) === false)
1424 ) {
1425 continue;
1426 }
1427 const templateConnectorId =
1428 connectorId > 0 && stationTemplate?.randomConnectors
1429 ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
1430 : connectorId;
1431 const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
1432 ChargingStationUtils.checkStationInfoConnectorStatus(
1433 templateConnectorId,
1434 connectorStatus,
1435 this.logPrefix(),
1436 this.templateFile
1437 );
1438 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1439 }
1440 ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1441 this.saveConnectorsStatus();
1442 } else {
1443 logger.warn(
1444 `${this.logPrefix()} Charging station information from template ${
1445 this.templateFile
1446 } with no connectors configuration defined, cannot create connectors`
1447 );
1448 }
1449 }
1450 } else {
1451 logger.warn(
1452 `${this.logPrefix()} Charging station information from template ${
1453 this.templateFile
1454 } with no connectors configuration defined, using already defined connectors`
1455 );
1456 }
1457 }
1458
1459 private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void {
1460 if (!stationTemplate?.Evses && this.evses.size === 0) {
1461 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1462 logger.error(`${this.logPrefix()} ${errorMsg}`);
1463 throw new BaseError(errorMsg);
1464 }
1465 if (!stationTemplate?.Evses[0]) {
1466 logger.warn(
1467 `${this.logPrefix()} Charging station information from template ${
1468 this.templateFile
1469 } with no evse id 0 configuration`
1470 );
1471 }
1472 if (!stationTemplate?.Evses[0]?.Connectors[0]) {
1473 logger.warn(
1474 `${this.logPrefix()} Charging station information from template ${
1475 this.templateFile
1476 } with evse id 0 with no connector id 0 configuration`
1477 );
1478 }
1479 if (stationTemplate?.Evses) {
1480 const evsesConfigHash = crypto
1481 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1482 .update(`${JSON.stringify(stationTemplate?.Evses)}`)
1483 .digest('hex');
1484 const evsesConfigChanged =
1485 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1486 if (this.evses?.size === 0 || evsesConfigChanged) {
1487 evsesConfigChanged && this.evses.clear();
1488 this.evsesConfigurationHash = evsesConfigHash;
1489 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationTemplate?.Evses);
1490 if (templateMaxEvses > 0) {
1491 for (const evse in stationTemplate.Evses) {
1492 const evseId = Utils.convertToInt(evse);
1493 this.evses.set(evseId, {
1494 connectors: ChargingStationUtils.buildConnectorsMap(
1495 stationTemplate?.Evses[evse]?.Connectors,
1496 this.logPrefix(),
1497 this.templateFile
1498 ),
1499 availability: AvailabilityType.Operative,
1500 });
1501 ChargingStationUtils.initializeConnectorsMapStatus(
1502 this.evses.get(evseId)?.connectors,
1503 this.logPrefix()
1504 );
1505 }
1506 this.saveEvsesStatus();
1507 } else {
1508 logger.warn(
1509 `${this.logPrefix()} Charging station information from template ${
1510 this.templateFile
1511 } with no evses configuration defined, cannot create evses`
1512 );
1513 }
1514 }
1515 } else {
1516 logger.warn(
1517 `${this.logPrefix()} Charging station information from template ${
1518 this.templateFile
1519 } with no evses configuration defined, using already defined evses`
1520 );
1521 }
1522 }
1523
1524 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1525 let configuration: ChargingStationConfiguration | undefined;
1526 if (Utils.isNotEmptyString(this.configurationFile) && fs.existsSync(this.configurationFile)) {
1527 try {
1528 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1529 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1530 this.configurationFileHash
1531 );
1532 } else {
1533 const measureId = `${FileType.ChargingStationConfiguration} read`;
1534 const beginId = PerformanceStatistics.beginMeasure(measureId);
1535 configuration = JSON.parse(
1536 fs.readFileSync(this.configurationFile, 'utf8')
1537 ) as ChargingStationConfiguration;
1538 PerformanceStatistics.endMeasure(measureId, beginId);
1539 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1540 this.configurationFileHash = configuration.configurationHash;
1541 }
1542 } catch (error) {
1543 ErrorUtils.handleFileException(
1544 this.configurationFile,
1545 FileType.ChargingStationConfiguration,
1546 error as NodeJS.ErrnoException,
1547 this.logPrefix()
1548 );
1549 }
1550 }
1551 return configuration;
1552 }
1553
1554 private saveChargingStationAutomaticTransactionGeneratorConfiguration(): void {
1555 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1556 this.saveConfiguration();
1557 }
1558 }
1559
1560 private saveConnectorsStatus() {
1561 this.saveConfiguration();
1562 }
1563
1564 private saveEvsesStatus() {
1565 this.saveConfiguration();
1566 }
1567
1568 private saveConfiguration(): void {
1569 if (Utils.isNotEmptyString(this.configurationFile)) {
1570 try {
1571 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1572 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1573 }
1574 let configurationData: ChargingStationConfiguration =
1575 Utils.cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()) ?? {};
1576 if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
1577 configurationData.stationInfo = this.stationInfo;
1578 } else {
1579 delete configurationData.stationInfo;
1580 }
1581 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
1582 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1583 } else {
1584 delete configurationData.configurationKey;
1585 }
1586 configurationData = merge<ChargingStationConfiguration>(
1587 configurationData,
1588 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1589 );
1590 if (
1591 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1592 !this.getAutomaticTransactionGeneratorConfiguration()
1593 ) {
1594 delete configurationData.automaticTransactionGenerator;
1595 }
1596 if (this.connectors.size > 0) {
1597 configurationData.connectorsStatus = buildConnectorsStatus(this);
1598 } else {
1599 delete configurationData.connectorsStatus;
1600 }
1601 if (this.evses.size > 0) {
1602 configurationData.evsesStatus = buildEvsesStatus(this);
1603 } else {
1604 delete configurationData.evsesStatus;
1605 }
1606 delete configurationData.configurationHash;
1607 const configurationHash = crypto
1608 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1609 .update(
1610 JSON.stringify({
1611 stationInfo: configurationData.stationInfo,
1612 configurationKey: configurationData.configurationKey,
1613 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1614 } as ChargingStationConfiguration)
1615 )
1616 .digest('hex');
1617 if (this.configurationFileHash !== configurationHash) {
1618 AsyncLock.acquire(AsyncLockType.configuration)
1619 .then(() => {
1620 configurationData.configurationHash = configurationHash;
1621 const measureId = `${FileType.ChargingStationConfiguration} write`;
1622 const beginId = PerformanceStatistics.beginMeasure(measureId);
1623 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1624 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1625 fs.closeSync(fileDescriptor);
1626 PerformanceStatistics.endMeasure(measureId, beginId);
1627 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1628 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1629 this.configurationFileHash = configurationHash;
1630 })
1631 .catch((error) => {
1632 ErrorUtils.handleFileException(
1633 this.configurationFile,
1634 FileType.ChargingStationConfiguration,
1635 error as NodeJS.ErrnoException,
1636 this.logPrefix()
1637 );
1638 })
1639 .finally(() => {
1640 AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
1641 });
1642 } else {
1643 logger.debug(
1644 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1645 this.configurationFile
1646 }`
1647 );
1648 }
1649 } catch (error) {
1650 ErrorUtils.handleFileException(
1651 this.configurationFile,
1652 FileType.ChargingStationConfiguration,
1653 error as NodeJS.ErrnoException,
1654 this.logPrefix()
1655 );
1656 }
1657 } else {
1658 logger.error(
1659 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1660 );
1661 }
1662 }
1663
1664 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1665 return this.getTemplateFromFile()?.Configuration;
1666 }
1667
1668 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1669 const configurationKey = this.getConfigurationFromFile()?.configurationKey;
1670 if (this.getOcppPersistentConfiguration() === true && configurationKey) {
1671 return { configurationKey };
1672 }
1673 return undefined;
1674 }
1675
1676 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1677 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1678 this.getOcppConfigurationFromFile();
1679 if (!ocppConfiguration) {
1680 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1681 }
1682 return ocppConfiguration;
1683 }
1684
1685 private async onOpen(): Promise<void> {
1686 if (this.isWebSocketConnectionOpened() === true) {
1687 logger.info(
1688 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1689 );
1690 if (this.isRegistered() === false) {
1691 // Send BootNotification
1692 let registrationRetryCount = 0;
1693 do {
1694 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1695 BootNotificationRequest,
1696 BootNotificationResponse
1697 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1698 skipBufferingOnError: true,
1699 });
1700 if (this.isRegistered() === false) {
1701 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
1702 await Utils.sleep(
1703 this?.bootNotificationResponse?.interval
1704 ? this.bootNotificationResponse.interval * 1000
1705 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1706 );
1707 }
1708 } while (
1709 this.isRegistered() === false &&
1710 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1711 this.getRegistrationMaxRetries() === -1)
1712 );
1713 }
1714 if (this.isRegistered() === true) {
1715 if (this.inAcceptedState() === true) {
1716 await this.startMessageSequence();
1717 }
1718 } else {
1719 logger.error(
1720 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1721 );
1722 }
1723 this.wsConnectionRestarted = false;
1724 this.autoReconnectRetryCount = 0;
1725 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1726 } else {
1727 logger.warn(
1728 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1729 );
1730 }
1731 }
1732
1733 private async onClose(code: number, reason: Buffer): Promise<void> {
1734 switch (code) {
1735 // Normal close
1736 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1737 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1738 logger.info(
1739 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1740 code
1741 )}' and reason '${reason.toString()}'`
1742 );
1743 this.autoReconnectRetryCount = 0;
1744 break;
1745 // Abnormal close
1746 default:
1747 logger.error(
1748 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1749 code
1750 )}' and reason '${reason.toString()}'`
1751 );
1752 this.started === true && (await this.reconnect());
1753 break;
1754 }
1755 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1756 }
1757
1758 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1759 const cachedRequest = this.requests.get(messageId);
1760 if (Array.isArray(cachedRequest) === true) {
1761 return cachedRequest;
1762 }
1763 throw new OCPPError(
1764 ErrorType.PROTOCOL_ERROR,
1765 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1766 messageType
1767 )} is not an array`,
1768 undefined,
1769 cachedRequest as JsonType
1770 );
1771 }
1772
1773 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1774 const [messageType, messageId, commandName, commandPayload] = request;
1775 if (this.getEnableStatistics() === true) {
1776 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1777 }
1778 logger.debug(
1779 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1780 request
1781 )}`
1782 );
1783 // Process the message
1784 await this.ocppIncomingRequestService.incomingRequestHandler(
1785 this,
1786 messageId,
1787 commandName,
1788 commandPayload
1789 );
1790 }
1791
1792 private handleResponseMessage(response: Response): void {
1793 const [messageType, messageId, commandPayload] = response;
1794 if (this.requests.has(messageId) === false) {
1795 // Error
1796 throw new OCPPError(
1797 ErrorType.INTERNAL_ERROR,
1798 `Response for unknown message id ${messageId}`,
1799 undefined,
1800 commandPayload
1801 );
1802 }
1803 // Respond
1804 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1805 messageType,
1806 messageId
1807 );
1808 logger.debug(
1809 `${this.logPrefix()} << Command '${
1810 requestCommandName ?? Constants.UNKNOWN_COMMAND
1811 }' received response payload: ${JSON.stringify(response)}`
1812 );
1813 responseCallback(commandPayload, requestPayload);
1814 }
1815
1816 private handleErrorMessage(errorResponse: ErrorResponse): void {
1817 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1818 if (this.requests.has(messageId) === false) {
1819 // Error
1820 throw new OCPPError(
1821 ErrorType.INTERNAL_ERROR,
1822 `Error response for unknown message id ${messageId}`,
1823 undefined,
1824 { errorType, errorMessage, errorDetails }
1825 );
1826 }
1827 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1828 logger.debug(
1829 `${this.logPrefix()} << Command '${
1830 requestCommandName ?? Constants.UNKNOWN_COMMAND
1831 }' received error response payload: ${JSON.stringify(errorResponse)}`
1832 );
1833 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1834 }
1835
1836 private async onMessage(data: RawData): Promise<void> {
1837 let request: IncomingRequest | Response | ErrorResponse;
1838 let messageType: number;
1839 let errorMsg: string;
1840 try {
1841 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1842 if (Array.isArray(request) === true) {
1843 [messageType] = request;
1844 // Check the type of message
1845 switch (messageType) {
1846 // Incoming Message
1847 case MessageType.CALL_MESSAGE:
1848 await this.handleIncomingMessage(request as IncomingRequest);
1849 break;
1850 // Response Message
1851 case MessageType.CALL_RESULT_MESSAGE:
1852 this.handleResponseMessage(request as Response);
1853 break;
1854 // Error Message
1855 case MessageType.CALL_ERROR_MESSAGE:
1856 this.handleErrorMessage(request as ErrorResponse);
1857 break;
1858 // Unknown Message
1859 default:
1860 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1861 errorMsg = `Wrong message type ${messageType}`;
1862 logger.error(`${this.logPrefix()} ${errorMsg}`);
1863 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
1864 }
1865 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1866 } else {
1867 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1868 request,
1869 });
1870 }
1871 } catch (error) {
1872 let commandName: IncomingRequestCommand;
1873 let requestCommandName: RequestCommand | IncomingRequestCommand;
1874 let errorCallback: ErrorCallback;
1875 const [, messageId] = request;
1876 switch (messageType) {
1877 case MessageType.CALL_MESSAGE:
1878 [, , commandName] = request as IncomingRequest;
1879 // Send error
1880 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1881 break;
1882 case MessageType.CALL_RESULT_MESSAGE:
1883 case MessageType.CALL_ERROR_MESSAGE:
1884 if (this.requests.has(messageId) === true) {
1885 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1886 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1887 errorCallback(error as OCPPError, false);
1888 } else {
1889 // Remove the request from the cache in case of error at response handling
1890 this.requests.delete(messageId);
1891 }
1892 break;
1893 }
1894 if (error instanceof OCPPError === false) {
1895 logger.warn(
1896 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1897 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1898 }' message '${data.toString()}' handling is not an OCPPError:`,
1899 error
1900 );
1901 }
1902 logger.error(
1903 `${this.logPrefix()} Incoming OCPP command '${
1904 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1905 }' message '${data.toString()}'${
1906 messageType !== MessageType.CALL_MESSAGE
1907 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1908 : ''
1909 } processing error:`,
1910 error
1911 );
1912 }
1913 }
1914
1915 private onPing(): void {
1916 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1917 }
1918
1919 private onPong(): void {
1920 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1921 }
1922
1923 private onError(error: WSError): void {
1924 this.closeWSConnection();
1925 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1926 }
1927
1928 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1929 if (this.getMeteringPerTransaction() === true) {
1930 return (
1931 (rounded === true
1932 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1933 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1934 );
1935 }
1936 return (
1937 (rounded === true
1938 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1939 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1940 );
1941 }
1942
1943 private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean {
1944 return stationTemplate?.useConnectorId0 ?? true;
1945 }
1946
1947 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1948 if (this.hasEvses) {
1949 for (const evseStatus of this.evses.values()) {
1950 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1951 if (connectorStatus.transactionStarted === true) {
1952 await this.stopTransactionOnConnector(connectorId, reason);
1953 }
1954 }
1955 }
1956 } else {
1957 for (const connectorId of this.connectors.keys()) {
1958 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1959 await this.stopTransactionOnConnector(connectorId, reason);
1960 }
1961 }
1962 }
1963 }
1964
1965 // 0 for disabling
1966 private getConnectionTimeout(): number {
1967 if (
1968 ChargingStationConfigurationUtils.getConfigurationKey(
1969 this,
1970 StandardParametersKey.ConnectionTimeOut
1971 )
1972 ) {
1973 return (
1974 parseInt(
1975 ChargingStationConfigurationUtils.getConfigurationKey(
1976 this,
1977 StandardParametersKey.ConnectionTimeOut
1978 ).value
1979 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1980 );
1981 }
1982 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1983 }
1984
1985 // -1 for unlimited, 0 for disabling
1986 private getAutoReconnectMaxRetries(): number | undefined {
1987 return (
1988 this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
1989 );
1990 }
1991
1992 // 0 for disabling
1993 private getRegistrationMaxRetries(): number | undefined {
1994 return this.stationInfo.registrationMaxRetries ?? -1;
1995 }
1996
1997 private getPowerDivider(): number {
1998 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors();
1999 if (this.stationInfo?.powerSharedByConnectors) {
2000 powerDivider = this.getNumberOfRunningTransactions();
2001 }
2002 return powerDivider;
2003 }
2004
2005 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
2006 const maximumPower = this.getMaximumPower(stationInfo);
2007 switch (this.getCurrentOutType(stationInfo)) {
2008 case CurrentType.AC:
2009 return ACElectricUtils.amperagePerPhaseFromPower(
2010 this.getNumberOfPhases(stationInfo),
2011 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2012 this.getVoltageOut(stationInfo)
2013 );
2014 case CurrentType.DC:
2015 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
2016 }
2017 }
2018
2019 private getAmperageLimitation(): number | undefined {
2020 if (
2021 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2022 ChargingStationConfigurationUtils.getConfigurationKey(
2023 this,
2024 this.stationInfo.amperageLimitationOcppKey
2025 )
2026 ) {
2027 return (
2028 Utils.convertToInt(
2029 ChargingStationConfigurationUtils.getConfigurationKey(
2030 this,
2031 this.stationInfo.amperageLimitationOcppKey
2032 )?.value
2033 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
2034 );
2035 }
2036 }
2037
2038 private async startMessageSequence(): Promise<void> {
2039 if (this.stationInfo?.autoRegister === true) {
2040 await this.ocppRequestService.requestHandler<
2041 BootNotificationRequest,
2042 BootNotificationResponse
2043 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2044 skipBufferingOnError: true,
2045 });
2046 }
2047 // Start WebSocket ping
2048 this.startWebSocketPing();
2049 // Start heartbeat
2050 this.startHeartbeat();
2051 // Initialize connectors status
2052 if (this.hasEvses) {
2053 for (const [evseId, evseStatus] of this.evses) {
2054 if (evseId > 0) {
2055 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2056 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2057 this,
2058 connectorId,
2059 connectorStatus
2060 );
2061 await OCPPServiceUtils.sendAndSetConnectorStatus(
2062 this,
2063 connectorId,
2064 connectorBootStatus,
2065 evseId
2066 );
2067 }
2068 }
2069 }
2070 } else {
2071 for (const connectorId of this.connectors.keys()) {
2072 if (connectorId > 0) {
2073 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2074 this,
2075 connectorId,
2076 this.getConnectorStatus(connectorId)
2077 );
2078 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2079 }
2080 }
2081 }
2082 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2083 await this.ocppRequestService.requestHandler<
2084 FirmwareStatusNotificationRequest,
2085 FirmwareStatusNotificationResponse
2086 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2087 status: FirmwareStatus.Installed,
2088 });
2089 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2090 }
2091
2092 // Start the ATG
2093 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2094 this.startAutomaticTransactionGenerator();
2095 }
2096 this.wsConnectionRestarted === true && this.flushMessageBuffer();
2097 }
2098
2099 private async stopMessageSequence(
2100 reason: StopTransactionReason = StopTransactionReason.NONE
2101 ): Promise<void> {
2102 // Stop WebSocket ping
2103 this.stopWebSocketPing();
2104 // Stop heartbeat
2105 this.stopHeartbeat();
2106 // Stop ongoing transactions
2107 if (this.automaticTransactionGenerator?.started === true) {
2108 this.stopAutomaticTransactionGenerator();
2109 } else {
2110 await this.stopRunningTransactions(reason);
2111 }
2112 if (this.hasEvses) {
2113 for (const [evseId, evseStatus] of this.evses) {
2114 if (evseId > 0) {
2115 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2116 await this.ocppRequestService.requestHandler<
2117 StatusNotificationRequest,
2118 StatusNotificationResponse
2119 >(
2120 this,
2121 RequestCommand.STATUS_NOTIFICATION,
2122 OCPPServiceUtils.buildStatusNotificationRequest(
2123 this,
2124 connectorId,
2125 ConnectorStatusEnum.Unavailable,
2126 evseId
2127 )
2128 );
2129 delete connectorStatus?.status;
2130 }
2131 }
2132 }
2133 } else {
2134 for (const connectorId of this.connectors.keys()) {
2135 if (connectorId > 0) {
2136 await this.ocppRequestService.requestHandler<
2137 StatusNotificationRequest,
2138 StatusNotificationResponse
2139 >(
2140 this,
2141 RequestCommand.STATUS_NOTIFICATION,
2142 OCPPServiceUtils.buildStatusNotificationRequest(
2143 this,
2144 connectorId,
2145 ConnectorStatusEnum.Unavailable
2146 )
2147 );
2148 delete this.getConnectorStatus(connectorId)?.status;
2149 }
2150 }
2151 }
2152 }
2153
2154 private startWebSocketPing(): void {
2155 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2156 this,
2157 StandardParametersKey.WebSocketPingInterval
2158 )
2159 ? Utils.convertToInt(
2160 ChargingStationConfigurationUtils.getConfigurationKey(
2161 this,
2162 StandardParametersKey.WebSocketPingInterval
2163 )?.value
2164 )
2165 : 0;
2166 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2167 this.webSocketPingSetInterval = setInterval(() => {
2168 if (this.isWebSocketConnectionOpened() === true) {
2169 this.wsConnection?.ping();
2170 }
2171 }, webSocketPingInterval * 1000);
2172 logger.info(
2173 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2174 webSocketPingInterval
2175 )}`
2176 );
2177 } else if (this.webSocketPingSetInterval) {
2178 logger.info(
2179 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2180 webSocketPingInterval
2181 )}`
2182 );
2183 } else {
2184 logger.error(
2185 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2186 );
2187 }
2188 }
2189
2190 private stopWebSocketPing(): void {
2191 if (this.webSocketPingSetInterval) {
2192 clearInterval(this.webSocketPingSetInterval);
2193 delete this.webSocketPingSetInterval;
2194 }
2195 }
2196
2197 private getConfiguredSupervisionUrl(): URL {
2198 let configuredSupervisionUrl: string;
2199 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2200 if (Utils.isNotEmptyArray(supervisionUrls)) {
2201 let configuredSupervisionUrlIndex: number;
2202 switch (Configuration.getSupervisionUrlDistribution()) {
2203 case SupervisionUrlDistribution.RANDOM:
2204 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2205 break;
2206 case SupervisionUrlDistribution.ROUND_ROBIN:
2207 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2208 default:
2209 Object.values(SupervisionUrlDistribution).includes(
2210 Configuration.getSupervisionUrlDistribution()
2211 ) === false &&
2212 logger.error(
2213 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2214 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2215 }`
2216 );
2217 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2218 break;
2219 }
2220 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex];
2221 } else {
2222 configuredSupervisionUrl = supervisionUrls as string;
2223 }
2224 if (Utils.isNotEmptyString(configuredSupervisionUrl)) {
2225 return new URL(configuredSupervisionUrl);
2226 }
2227 const errorMsg = 'No supervision url(s) configured';
2228 logger.error(`${this.logPrefix()} ${errorMsg}`);
2229 throw new BaseError(`${errorMsg}`);
2230 }
2231
2232 private stopHeartbeat(): void {
2233 if (this.heartbeatSetInterval) {
2234 clearInterval(this.heartbeatSetInterval);
2235 delete this.heartbeatSetInterval;
2236 }
2237 }
2238
2239 private terminateWSConnection(): void {
2240 if (this.isWebSocketConnectionOpened() === true) {
2241 this.wsConnection?.terminate();
2242 this.wsConnection = null;
2243 }
2244 }
2245
2246 private getReconnectExponentialDelay(): boolean {
2247 return this.stationInfo?.reconnectExponentialDelay ?? false;
2248 }
2249
2250 private async reconnect(): Promise<void> {
2251 // Stop WebSocket ping
2252 this.stopWebSocketPing();
2253 // Stop heartbeat
2254 this.stopHeartbeat();
2255 // Stop the ATG if needed
2256 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
2257 this.stopAutomaticTransactionGenerator();
2258 }
2259 if (
2260 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2261 this.getAutoReconnectMaxRetries() === -1
2262 ) {
2263 ++this.autoReconnectRetryCount;
2264 const reconnectDelay = this.getReconnectExponentialDelay()
2265 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2266 : this.getConnectionTimeout() * 1000;
2267 const reconnectDelayWithdraw = 1000;
2268 const reconnectTimeout =
2269 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2270 ? reconnectDelay - reconnectDelayWithdraw
2271 : 0;
2272 logger.error(
2273 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2274 reconnectDelay,
2275 2
2276 )}ms, timeout ${reconnectTimeout}ms`
2277 );
2278 await Utils.sleep(reconnectDelay);
2279 logger.error(
2280 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2281 );
2282 this.openWSConnection(
2283 {
2284 ...(this.stationInfo?.wsOptions ?? {}),
2285 handshakeTimeout: reconnectTimeout,
2286 },
2287 { closeOpened: true }
2288 );
2289 this.wsConnectionRestarted = true;
2290 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2291 logger.error(
2292 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2293 this.autoReconnectRetryCount
2294 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2295 );
2296 }
2297 }
2298 }