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