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