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