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