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