feat: add connector status handling at boot with evses configuration
[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.has(0) ? this.connectors.size - 1 : this.connectors.size;
267 }
268
269 public getNumberOfEvses(): number {
270 return this.evses.has(0) ? this.evses.size - 1 : 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 templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
1328 stationInfo.Connectors
1329 );
1330 ChargingStationUtils.checkTemplateMaxConnectors(
1331 templateMaxConnectors,
1332 this.templateFile,
1333 this.logPrefix()
1334 );
1335 const templateMaxAvailableConnectors = stationInfo?.Connectors[0]
1336 ? templateMaxConnectors - 1
1337 : templateMaxConnectors;
1338 if (
1339 configuredMaxConnectors > templateMaxAvailableConnectors &&
1340 !stationInfo?.randomConnectors
1341 ) {
1342 logger.warn(
1343 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
1344 this.templateFile
1345 }, forcing random connector configurations affectation`
1346 );
1347 stationInfo.randomConnectors = true;
1348 }
1349 if (templateMaxConnectors > 0) {
1350 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1351 if (
1352 connectorId === 0 &&
1353 (!stationInfo?.Connectors[connectorId] ||
1354 this.getUseConnectorId0(stationInfo) === false)
1355 ) {
1356 continue;
1357 }
1358 const templateConnectorId =
1359 connectorId > 0 && stationInfo?.randomConnectors
1360 ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
1361 : connectorId;
1362 const connectorStatus = stationInfo?.Connectors[templateConnectorId];
1363 ChargingStationUtils.checkStationInfoConnectorStatus(
1364 templateConnectorId,
1365 connectorStatus,
1366 this.logPrefix(),
1367 this.templateFile
1368 );
1369 this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
1370 }
1371 ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
1372 this.saveConnectorsStatus();
1373 } else {
1374 logger.warn(
1375 `${this.logPrefix()} Charging station information from template ${
1376 this.templateFile
1377 } with no connectors configuration defined, cannot create connectors`
1378 );
1379 }
1380 }
1381 } else {
1382 logger.warn(
1383 `${this.logPrefix()} Charging station information from template ${
1384 this.templateFile
1385 } with no connectors configuration defined, using already defined connectors`
1386 );
1387 }
1388 }
1389
1390 private initializeEvses(stationInfo: ChargingStationInfo): void {
1391 if (!stationInfo?.Evses && this.evses.size === 0) {
1392 const logMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1393 logger.error(`${this.logPrefix()} ${logMsg}`);
1394 throw new BaseError(logMsg);
1395 }
1396 if (!stationInfo?.Evses[0]) {
1397 logger.warn(
1398 `${this.logPrefix()} Charging station information from template ${
1399 this.templateFile
1400 } with no evse id 0 configuration`
1401 );
1402 }
1403 if (!stationInfo?.Evses[0]?.Connectors[0]) {
1404 logger.warn(
1405 `${this.logPrefix()} Charging station information from template ${
1406 this.templateFile
1407 } with evse id 0 with no connector id 0 configuration`
1408 );
1409 }
1410 if (stationInfo?.Evses) {
1411 const evsesConfigHash = crypto
1412 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1413 .update(`${JSON.stringify(stationInfo?.Evses)}`)
1414 .digest('hex');
1415 const evsesConfigChanged =
1416 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
1417 if (this.evses?.size === 0 || evsesConfigChanged) {
1418 evsesConfigChanged && this.evses.clear();
1419 this.evsesConfigurationHash = evsesConfigHash;
1420 const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationInfo?.Evses);
1421 if (templateMaxEvses > 0) {
1422 for (const evse in stationInfo.Evses) {
1423 const evseId = Utils.convertToInt(evse);
1424 this.evses.set(evseId, {
1425 connectors: ChargingStationUtils.buildConnectorsMap(
1426 stationInfo?.Evses[evse]?.Connectors,
1427 this.logPrefix(),
1428 this.templateFile
1429 ),
1430 availability: AvailabilityType.Operative,
1431 });
1432 ChargingStationUtils.initializeConnectorsMapStatus(
1433 this.evses.get(evseId)?.connectors,
1434 this.logPrefix()
1435 );
1436 }
1437 this.saveEvsesStatus();
1438 } else {
1439 logger.warn(
1440 `${this.logPrefix()} Charging station information from template ${
1441 this.templateFile
1442 } with no evses configuration defined, cannot create evses`
1443 );
1444 }
1445 }
1446 } else {
1447 logger.warn(
1448 `${this.logPrefix()} Charging station information from template ${
1449 this.templateFile
1450 } with no evses configuration defined, using already defined evses`
1451 );
1452 }
1453 }
1454
1455 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1456 let configuration: ChargingStationConfiguration | undefined;
1457 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1458 try {
1459 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1460 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1461 this.configurationFileHash
1462 );
1463 } else {
1464 const measureId = `${FileType.ChargingStationConfiguration} read`;
1465 const beginId = PerformanceStatistics.beginMeasure(measureId);
1466 configuration = JSON.parse(
1467 fs.readFileSync(this.configurationFile, 'utf8')
1468 ) as ChargingStationConfiguration;
1469 PerformanceStatistics.endMeasure(measureId, beginId);
1470 this.configurationFileHash = configuration.configurationHash;
1471 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1472 }
1473 } catch (error) {
1474 FileUtils.handleFileException(
1475 this.configurationFile,
1476 FileType.ChargingStationConfiguration,
1477 error as NodeJS.ErrnoException,
1478 this.logPrefix()
1479 );
1480 }
1481 }
1482 return configuration;
1483 }
1484
1485 private saveConnectorsStatus() {
1486 if (this.getOcppPersistentConfiguration()) {
1487 this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, evses: false });
1488 }
1489 }
1490
1491 private saveEvsesStatus() {
1492 if (this.getOcppPersistentConfiguration()) {
1493 this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, connectors: false });
1494 }
1495 }
1496
1497 private saveConfiguration(
1498 params: {
1499 stationInfo?: boolean;
1500 ocppConfiguration?: boolean;
1501 connectors?: boolean;
1502 evses?: boolean;
1503 } = { stationInfo: true, ocppConfiguration: true, connectors: true, evses: true }
1504 ): void {
1505 if (this.configurationFile) {
1506 params = {
1507 ...params,
1508 ...{ stationInfo: true, ocppConfiguration: true, connectors: true, evses: true },
1509 };
1510 try {
1511 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1512 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1513 }
1514 const configurationData: ChargingStationConfiguration =
1515 Utils.cloneObject(this.getConfigurationFromFile()) ?? {};
1516 if (params.stationInfo && this.stationInfo) {
1517 configurationData.stationInfo = this.stationInfo;
1518 }
1519 if (params.ocppConfiguration && this.ocppConfiguration?.configurationKey) {
1520 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1521 }
1522 if (params.connectors && this.connectors.size > 0) {
1523 configurationData.connectorsStatus = [...this.connectors.values()].map(
1524 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1525 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1526 );
1527 }
1528 if (params.evses && this.evses.size > 0) {
1529 configurationData.evsesStatus = [...this.evses.values()].map((evseStatus) => {
1530 const status = {
1531 ...evseStatus,
1532 connectorsStatus: [...evseStatus.connectors.values()].map(
1533 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1534 ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
1535 ),
1536 };
1537 delete status.connectors;
1538 return status as EvseStatusConfiguration;
1539 });
1540 }
1541 delete configurationData.configurationHash;
1542 const configurationHash = crypto
1543 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1544 .update(JSON.stringify(configurationData))
1545 .digest('hex');
1546 if (this.configurationFileHash !== configurationHash) {
1547 configurationData.configurationHash = configurationHash;
1548 const measureId = `${FileType.ChargingStationConfiguration} write`;
1549 const beginId = PerformanceStatistics.beginMeasure(measureId);
1550 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1551 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1552 fs.closeSync(fileDescriptor);
1553 PerformanceStatistics.endMeasure(measureId, beginId);
1554 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1555 this.configurationFileHash = configurationHash;
1556 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1557 } else {
1558 logger.debug(
1559 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1560 this.configurationFile
1561 }`
1562 );
1563 }
1564 } catch (error) {
1565 FileUtils.handleFileException(
1566 this.configurationFile,
1567 FileType.ChargingStationConfiguration,
1568 error as NodeJS.ErrnoException,
1569 this.logPrefix()
1570 );
1571 }
1572 } else {
1573 logger.error(
1574 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1575 );
1576 }
1577 }
1578
1579 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1580 return this.getTemplateFromFile()?.Configuration;
1581 }
1582
1583 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1584 let configuration: ChargingStationConfiguration | undefined;
1585 if (this.getOcppPersistentConfiguration() === true) {
1586 const configurationFromFile = this.getConfigurationFromFile();
1587 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1588 }
1589 if (!Utils.isNullOrUndefined(configuration)) {
1590 delete configuration.stationInfo;
1591 delete configuration.configurationHash;
1592 }
1593 return configuration;
1594 }
1595
1596 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1597 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1598 this.getOcppConfigurationFromFile();
1599 if (!ocppConfiguration) {
1600 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1601 }
1602 return ocppConfiguration;
1603 }
1604
1605 private async onOpen(): Promise<void> {
1606 if (this.isWebSocketConnectionOpened() === true) {
1607 logger.info(
1608 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1609 );
1610 if (this.isRegistered() === false) {
1611 // Send BootNotification
1612 let registrationRetryCount = 0;
1613 do {
1614 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1615 BootNotificationRequest,
1616 BootNotificationResponse
1617 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1618 skipBufferingOnError: true,
1619 });
1620 if (this.isRegistered() === false) {
1621 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1622 await Utils.sleep(
1623 this?.bootNotificationResponse?.interval
1624 ? this.bootNotificationResponse.interval * 1000
1625 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1626 );
1627 }
1628 } while (
1629 this.isRegistered() === false &&
1630 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1631 this.getRegistrationMaxRetries() === -1)
1632 );
1633 }
1634 if (this.isRegistered() === true) {
1635 if (this.isInAcceptedState() === true) {
1636 await this.startMessageSequence();
1637 }
1638 } else {
1639 logger.error(
1640 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1641 );
1642 }
1643 this.wsConnectionRestarted = false;
1644 this.autoReconnectRetryCount = 0;
1645 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1646 } else {
1647 logger.warn(
1648 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1649 );
1650 }
1651 }
1652
1653 private async onClose(code: number, reason: Buffer): Promise<void> {
1654 switch (code) {
1655 // Normal close
1656 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1657 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1658 logger.info(
1659 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1660 code
1661 )}' and reason '${reason.toString()}'`
1662 );
1663 this.autoReconnectRetryCount = 0;
1664 break;
1665 // Abnormal close
1666 default:
1667 logger.error(
1668 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1669 code
1670 )}' and reason '${reason.toString()}'`
1671 );
1672 this.started === true && (await this.reconnect());
1673 break;
1674 }
1675 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1676 }
1677
1678 private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
1679 const cachedRequest = this.requests.get(messageId);
1680 if (Array.isArray(cachedRequest) === true) {
1681 return cachedRequest;
1682 }
1683 throw new OCPPError(
1684 ErrorType.PROTOCOL_ERROR,
1685 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1686 messageType
1687 )} is not an array`,
1688 undefined,
1689 cachedRequest as JsonType
1690 );
1691 }
1692
1693 private async handleIncomingMessage(request: IncomingRequest): Promise<void> {
1694 const [messageType, messageId, commandName, commandPayload] = request;
1695 if (this.getEnableStatistics() === true) {
1696 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
1697 }
1698 logger.debug(
1699 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1700 request
1701 )}`
1702 );
1703 // Process the message
1704 await this.ocppIncomingRequestService.incomingRequestHandler(
1705 this,
1706 messageId,
1707 commandName,
1708 commandPayload
1709 );
1710 }
1711
1712 private handleResponseMessage(response: Response): void {
1713 const [messageType, messageId, commandPayload] = response;
1714 if (this.requests.has(messageId) === false) {
1715 // Error
1716 throw new OCPPError(
1717 ErrorType.INTERNAL_ERROR,
1718 `Response for unknown message id ${messageId}`,
1719 undefined,
1720 commandPayload
1721 );
1722 }
1723 // Respond
1724 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1725 messageType,
1726 messageId
1727 );
1728 logger.debug(
1729 `${this.logPrefix()} << Command '${
1730 requestCommandName ?? Constants.UNKNOWN_COMMAND
1731 }' received response payload: ${JSON.stringify(response)}`
1732 );
1733 responseCallback(commandPayload, requestPayload);
1734 }
1735
1736 private handleErrorMessage(errorResponse: ErrorResponse): void {
1737 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse;
1738 if (this.requests.has(messageId) === false) {
1739 // Error
1740 throw new OCPPError(
1741 ErrorType.INTERNAL_ERROR,
1742 `Error response for unknown message id ${messageId}`,
1743 undefined,
1744 { errorType, errorMessage, errorDetails }
1745 );
1746 }
1747 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1748 logger.debug(
1749 `${this.logPrefix()} << Command '${
1750 requestCommandName ?? Constants.UNKNOWN_COMMAND
1751 }' received error response payload: ${JSON.stringify(errorResponse)}`
1752 );
1753 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1754 }
1755
1756 private async onMessage(data: RawData): Promise<void> {
1757 let request: IncomingRequest | Response | ErrorResponse;
1758 let messageType: number;
1759 let errMsg: string;
1760 try {
1761 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1762 if (Array.isArray(request) === true) {
1763 [messageType] = request;
1764 // Check the type of message
1765 switch (messageType) {
1766 // Incoming Message
1767 case MessageType.CALL_MESSAGE:
1768 await this.handleIncomingMessage(request as IncomingRequest);
1769 break;
1770 // Response Message
1771 case MessageType.CALL_RESULT_MESSAGE:
1772 this.handleResponseMessage(request as Response);
1773 break;
1774 // Error Message
1775 case MessageType.CALL_ERROR_MESSAGE:
1776 this.handleErrorMessage(request as ErrorResponse);
1777 break;
1778 // Unknown Message
1779 default:
1780 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1781 errMsg = `Wrong message type ${messageType}`;
1782 logger.error(`${this.logPrefix()} ${errMsg}`);
1783 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1784 }
1785 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1786 } else {
1787 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1788 request,
1789 });
1790 }
1791 } catch (error) {
1792 let commandName: IncomingRequestCommand;
1793 let requestCommandName: RequestCommand | IncomingRequestCommand;
1794 let errorCallback: ErrorCallback;
1795 const [, messageId] = request;
1796 switch (messageType) {
1797 case MessageType.CALL_MESSAGE:
1798 [, , commandName] = request as IncomingRequest;
1799 // Send error
1800 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName);
1801 break;
1802 case MessageType.CALL_RESULT_MESSAGE:
1803 case MessageType.CALL_ERROR_MESSAGE:
1804 if (this.requests.has(messageId) === true) {
1805 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId);
1806 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1807 errorCallback(error as OCPPError, false);
1808 } else {
1809 // Remove the request from the cache in case of error at response handling
1810 this.requests.delete(messageId);
1811 }
1812 break;
1813 }
1814 if (error instanceof OCPPError === false) {
1815 logger.warn(
1816 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1817 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1818 }' message '${data.toString()}' handling is not an OCPPError:`,
1819 error
1820 );
1821 }
1822 logger.error(
1823 `${this.logPrefix()} Incoming OCPP command '${
1824 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1825 }' message '${data.toString()}'${
1826 messageType !== MessageType.CALL_MESSAGE
1827 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1828 : ''
1829 } processing error:`,
1830 error
1831 );
1832 }
1833 }
1834
1835 private onPing(): void {
1836 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1837 }
1838
1839 private onPong(): void {
1840 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1841 }
1842
1843 private onError(error: WSError): void {
1844 this.closeWSConnection();
1845 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1846 }
1847
1848 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1849 if (this.getMeteringPerTransaction() === true) {
1850 return (
1851 (rounded === true
1852 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1853 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1854 );
1855 }
1856 return (
1857 (rounded === true
1858 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1859 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1860 );
1861 }
1862
1863 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1864 const localStationInfo = stationInfo ?? this.stationInfo;
1865 return localStationInfo?.useConnectorId0 ?? true;
1866 }
1867
1868 private getNumberOfRunningTransactions(): number {
1869 let trxCount = 0;
1870 if (this.hasEvses) {
1871 for (const evseStatus of this.evses.values()) {
1872 for (const connectorStatus of evseStatus.connectors.values()) {
1873 if (connectorStatus.transactionStarted === true) {
1874 trxCount++;
1875 }
1876 }
1877 }
1878 } else {
1879 for (const connectorId of this.connectors.keys()) {
1880 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1881 trxCount++;
1882 }
1883 }
1884 }
1885 return trxCount;
1886 }
1887
1888 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1889 if (this.hasEvses) {
1890 for (const evseStatus of this.evses.values()) {
1891 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1892 if (connectorStatus.transactionStarted === true) {
1893 await this.stopTransactionOnConnector(connectorId, reason);
1894 }
1895 }
1896 }
1897 } else {
1898 for (const connectorId of this.connectors.keys()) {
1899 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1900 await this.stopTransactionOnConnector(connectorId, reason);
1901 }
1902 }
1903 }
1904 }
1905
1906 // 0 for disabling
1907 private getConnectionTimeout(): number {
1908 if (
1909 ChargingStationConfigurationUtils.getConfigurationKey(
1910 this,
1911 StandardParametersKey.ConnectionTimeOut
1912 )
1913 ) {
1914 return (
1915 parseInt(
1916 ChargingStationConfigurationUtils.getConfigurationKey(
1917 this,
1918 StandardParametersKey.ConnectionTimeOut
1919 ).value
1920 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1921 );
1922 }
1923 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1924 }
1925
1926 // -1 for unlimited, 0 for disabling
1927 private getAutoReconnectMaxRetries(): number | undefined {
1928 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1929 return this.stationInfo.autoReconnectMaxRetries;
1930 }
1931 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1932 return Configuration.getAutoReconnectMaxRetries();
1933 }
1934 return -1;
1935 }
1936
1937 // 0 for disabling
1938 private getRegistrationMaxRetries(): number | undefined {
1939 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1940 return this.stationInfo.registrationMaxRetries;
1941 }
1942 return -1;
1943 }
1944
1945 private getPowerDivider(): number {
1946 let powerDivider = this.getNumberOfConnectors();
1947 if (this.stationInfo?.powerSharedByConnectors) {
1948 powerDivider = this.getNumberOfRunningTransactions();
1949 }
1950 return powerDivider;
1951 }
1952
1953 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1954 const maximumPower = this.getMaximumPower(stationInfo);
1955 switch (this.getCurrentOutType(stationInfo)) {
1956 case CurrentType.AC:
1957 return ACElectricUtils.amperagePerPhaseFromPower(
1958 this.getNumberOfPhases(stationInfo),
1959 maximumPower / this.getNumberOfConnectors(),
1960 this.getVoltageOut(stationInfo)
1961 );
1962 case CurrentType.DC:
1963 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1964 }
1965 }
1966
1967 private getAmperageLimitation(): number | undefined {
1968 if (
1969 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1970 ChargingStationConfigurationUtils.getConfigurationKey(
1971 this,
1972 this.stationInfo.amperageLimitationOcppKey
1973 )
1974 ) {
1975 return (
1976 Utils.convertToInt(
1977 ChargingStationConfigurationUtils.getConfigurationKey(
1978 this,
1979 this.stationInfo.amperageLimitationOcppKey
1980 )?.value
1981 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1982 );
1983 }
1984 }
1985
1986 private async startMessageSequence(): Promise<void> {
1987 if (this.stationInfo?.autoRegister === true) {
1988 await this.ocppRequestService.requestHandler<
1989 BootNotificationRequest,
1990 BootNotificationResponse
1991 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1992 skipBufferingOnError: true,
1993 });
1994 }
1995 // Start WebSocket ping
1996 this.startWebSocketPing();
1997 // Start heartbeat
1998 this.startHeartbeat();
1999 // Initialize connectors status
2000 if (this.hasEvses) {
2001 for (const [evseId, evseStatus] of this.evses) {
2002 if (evseId === 0) {
2003 continue;
2004 }
2005 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2006 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2007 this,
2008 connectorId,
2009 connectorStatus
2010 );
2011 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2012 }
2013 }
2014 } else {
2015 for (const connectorId of this.connectors.keys()) {
2016 if (connectorId === 0) {
2017 continue;
2018 }
2019 const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
2020 this,
2021 connectorId,
2022 this.getConnectorStatus(connectorId)
2023 );
2024 await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorBootStatus);
2025 }
2026 }
2027 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2028 await this.ocppRequestService.requestHandler<
2029 FirmwareStatusNotificationRequest,
2030 FirmwareStatusNotificationResponse
2031 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2032 status: FirmwareStatus.Installed,
2033 });
2034 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
2035 }
2036
2037 // Start the ATG
2038 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
2039 this.startAutomaticTransactionGenerator();
2040 }
2041 this.wsConnectionRestarted === true && this.flushMessageBuffer();
2042 }
2043
2044 private async stopMessageSequence(
2045 reason: StopTransactionReason = StopTransactionReason.NONE
2046 ): Promise<void> {
2047 // Stop WebSocket ping
2048 this.stopWebSocketPing();
2049 // Stop heartbeat
2050 this.stopHeartbeat();
2051 // Stop ongoing transactions
2052 if (this.automaticTransactionGenerator?.started === true) {
2053 this.stopAutomaticTransactionGenerator();
2054 } else {
2055 await this.stopRunningTransactions(reason);
2056 }
2057 for (const connectorId of this.connectors.keys()) {
2058 if (connectorId > 0) {
2059 await this.ocppRequestService.requestHandler<
2060 StatusNotificationRequest,
2061 StatusNotificationResponse
2062 >(
2063 this,
2064 RequestCommand.STATUS_NOTIFICATION,
2065 OCPPServiceUtils.buildStatusNotificationRequest(
2066 this,
2067 connectorId,
2068 ConnectorStatusEnum.Unavailable
2069 )
2070 );
2071 delete this.getConnectorStatus(connectorId)?.status;
2072 }
2073 }
2074 }
2075
2076 private startWebSocketPing(): void {
2077 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
2078 this,
2079 StandardParametersKey.WebSocketPingInterval
2080 )
2081 ? Utils.convertToInt(
2082 ChargingStationConfigurationUtils.getConfigurationKey(
2083 this,
2084 StandardParametersKey.WebSocketPingInterval
2085 )?.value
2086 )
2087 : 0;
2088 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
2089 this.webSocketPingSetInterval = setInterval(() => {
2090 if (this.isWebSocketConnectionOpened() === true) {
2091 this.wsConnection?.ping();
2092 }
2093 }, webSocketPingInterval * 1000);
2094 logger.info(
2095 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2096 webSocketPingInterval
2097 )}`
2098 );
2099 } else if (this.webSocketPingSetInterval) {
2100 logger.info(
2101 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2102 webSocketPingInterval
2103 )}`
2104 );
2105 } else {
2106 logger.error(
2107 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2108 );
2109 }
2110 }
2111
2112 private stopWebSocketPing(): void {
2113 if (this.webSocketPingSetInterval) {
2114 clearInterval(this.webSocketPingSetInterval);
2115 delete this.webSocketPingSetInterval;
2116 }
2117 }
2118
2119 private getConfiguredSupervisionUrl(): URL {
2120 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
2121 if (Utils.isNotEmptyArray(supervisionUrls)) {
2122 let configuredSupervisionUrlIndex: number;
2123 switch (Configuration.getSupervisionUrlDistribution()) {
2124 case SupervisionUrlDistribution.RANDOM:
2125 configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
2126 break;
2127 case SupervisionUrlDistribution.ROUND_ROBIN:
2128 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2129 default:
2130 Object.values(SupervisionUrlDistribution).includes(
2131 Configuration.getSupervisionUrlDistribution()
2132 ) === false &&
2133 logger.error(
2134 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2135 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2136 }`
2137 );
2138 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2139 break;
2140 }
2141 return new URL(supervisionUrls[configuredSupervisionUrlIndex]);
2142 }
2143 return new URL(supervisionUrls as string);
2144 }
2145
2146 private stopHeartbeat(): void {
2147 if (this.heartbeatSetInterval) {
2148 clearInterval(this.heartbeatSetInterval);
2149 delete this.heartbeatSetInterval;
2150 }
2151 }
2152
2153 private terminateWSConnection(): void {
2154 if (this.isWebSocketConnectionOpened() === true) {
2155 this.wsConnection?.terminate();
2156 this.wsConnection = null;
2157 }
2158 }
2159
2160 private getReconnectExponentialDelay(): boolean {
2161 return this.stationInfo?.reconnectExponentialDelay ?? false;
2162 }
2163
2164 private async reconnect(): Promise<void> {
2165 // Stop WebSocket ping
2166 this.stopWebSocketPing();
2167 // Stop heartbeat
2168 this.stopHeartbeat();
2169 // Stop the ATG if needed
2170 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2171 this.stopAutomaticTransactionGenerator();
2172 }
2173 if (
2174 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2175 this.getAutoReconnectMaxRetries() === -1
2176 ) {
2177 this.autoReconnectRetryCount++;
2178 const reconnectDelay = this.getReconnectExponentialDelay()
2179 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2180 : this.getConnectionTimeout() * 1000;
2181 const reconnectDelayWithdraw = 1000;
2182 const reconnectTimeout =
2183 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2184 ? reconnectDelay - reconnectDelayWithdraw
2185 : 0;
2186 logger.error(
2187 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2188 reconnectDelay,
2189 2
2190 )}ms, timeout ${reconnectTimeout}ms`
2191 );
2192 await Utils.sleep(reconnectDelay);
2193 logger.error(
2194 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2195 );
2196 this.openWSConnection(
2197 {
2198 ...(this.stationInfo?.wsOptions ?? {}),
2199 handshakeTimeout: reconnectTimeout,
2200 },
2201 { closeOpened: true }
2202 );
2203 this.wsConnectionRestarted = true;
2204 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2205 logger.error(
2206 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2207 this.autoReconnectRetryCount
2208 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2209 );
2210 }
2211 }
2212
2213 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2214 | AutomaticTransactionGeneratorConfiguration
2215 | undefined {
2216 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
2217 }
2218 }