Apply prettier formating
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
2
3 import {
4 AvailabilityType,
5 BootNotificationRequest,
6 CachedRequest,
7 IncomingRequest,
8 IncomingRequestCommand,
9 RequestCommand,
10 } from '../types/ocpp/Requests';
11 import { BootNotificationResponse, RegistrationStatus } from '../types/ocpp/Responses';
12 import ChargingStationConfiguration, {
13 ConfigurationKey,
14 } from '../types/ChargingStationConfiguration';
15 import ChargingStationTemplate, {
16 CurrentType,
17 PowerUnits,
18 Voltage,
19 } from '../types/ChargingStationTemplate';
20 import {
21 ConnectorPhaseRotation,
22 StandardParametersKey,
23 SupportedFeatureProfiles,
24 VendorDefaultParametersKey,
25 } from '../types/ocpp/Configuration';
26 import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
27 import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket';
28 import WebSocket, { ClientOptions, Data, OPEN } from 'ws';
29
30 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
31 import { ChargePointStatus } from '../types/ocpp/ChargePointStatus';
32 import { ChargingProfile } from '../types/ocpp/ChargingProfile';
33 import ChargingStationInfo from '../types/ChargingStationInfo';
34 import { ChargingStationWorkerMessageEvents } from '../types/ChargingStationWorker';
35 import { ClientRequestArgs } from 'http';
36 import Configuration from '../utils/Configuration';
37 import { ConnectorStatus } from '../types/ConnectorStatus';
38 import Constants from '../utils/Constants';
39 import { ErrorType } from '../types/ocpp/ErrorType';
40 import FileUtils from '../utils/FileUtils';
41 import { JsonType } from '../types/JsonType';
42 import { MessageType } from '../types/ocpp/MessageType';
43 import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService';
44 import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService';
45 import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService';
46 import OCPPError from '../exception/OCPPError';
47 import OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService';
48 import OCPPRequestService from './ocpp/OCPPRequestService';
49 import { OCPPVersion } from '../types/ocpp/OCPPVersion';
50 import PerformanceStatistics from '../performance/PerformanceStatistics';
51 import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
52 import { StopTransactionReason } from '../types/ocpp/Transaction';
53 import { SupervisionUrlDistribution } from '../types/ConfigurationData';
54 import { URL } from 'url';
55 import Utils from '../utils/Utils';
56 import crypto from 'crypto';
57 import fs from 'fs';
58 import logger from '../utils/Logger';
59 import { parentPort } from 'worker_threads';
60 import path from 'path';
61
62 export default class ChargingStation {
63 public readonly id: string;
64 public readonly stationTemplateFile: string;
65 public authorizedTags: string[];
66 public stationInfo!: ChargingStationInfo;
67 public readonly connectors: Map<number, ConnectorStatus>;
68 public configuration!: ChargingStationConfiguration;
69 public wsConnection!: WebSocket;
70 public readonly requests: Map<string, CachedRequest>;
71 public performanceStatistics!: PerformanceStatistics;
72 public heartbeatSetInterval!: NodeJS.Timeout;
73 public ocppRequestService!: OCPPRequestService;
74 private readonly index: number;
75 private bootNotificationRequest!: BootNotificationRequest;
76 private bootNotificationResponse!: BootNotificationResponse | null;
77 private connectorsConfigurationHash!: string;
78 private ocppIncomingRequestService!: OCPPIncomingRequestService;
79 private readonly messageBuffer: Set<string>;
80 private wsConfiguredConnectionUrl!: URL;
81 private wsConnectionRestarted: boolean;
82 private stopped: boolean;
83 private autoReconnectRetryCount: number;
84 private automaticTransactionGenerator!: AutomaticTransactionGenerator;
85 private webSocketPingSetInterval!: NodeJS.Timeout;
86
87 constructor(index: number, stationTemplateFile: string) {
88 this.id = Utils.generateUUID();
89 this.index = index;
90 this.stationTemplateFile = stationTemplateFile;
91 this.stopped = false;
92 this.wsConnectionRestarted = false;
93 this.autoReconnectRetryCount = 0;
94 this.connectors = new Map<number, ConnectorStatus>();
95 this.requests = new Map<string, CachedRequest>();
96 this.messageBuffer = new Set<string>();
97 this.initialize();
98 this.authorizedTags = this.getAuthorizedTags();
99 }
100
101 get wsConnectionUrl(): URL {
102 return this.getSupervisionUrlOcppConfiguration()
103 ? new URL(
104 this.getConfigurationKey(
105 this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl
106 ).value +
107 '/' +
108 this.stationInfo.chargingStationId
109 )
110 : this.wsConfiguredConnectionUrl;
111 }
112
113 public logPrefix(): string {
114 return Utils.logPrefix(` ${this.stationInfo.chargingStationId} |`);
115 }
116
117 public getBootNotificationRequest(): BootNotificationRequest {
118 return this.bootNotificationRequest;
119 }
120
121 public getRandomIdTag(): string {
122 const index = Math.floor(Utils.secureRandom() * this.authorizedTags.length);
123 return this.authorizedTags[index];
124 }
125
126 public hasAuthorizedTags(): boolean {
127 return !Utils.isEmptyArray(this.authorizedTags);
128 }
129
130 public getEnableStatistics(): boolean | undefined {
131 return !Utils.isUndefined(this.stationInfo.enableStatistics)
132 ? this.stationInfo.enableStatistics
133 : true;
134 }
135
136 public getMayAuthorizeAtRemoteStart(): boolean | undefined {
137 return this.stationInfo.mayAuthorizeAtRemoteStart ?? true;
138 }
139
140 public getNumberOfPhases(): number | undefined {
141 switch (this.getCurrentOutType()) {
142 case CurrentType.AC:
143 return !Utils.isUndefined(this.stationInfo.numberOfPhases)
144 ? this.stationInfo.numberOfPhases
145 : 3;
146 case CurrentType.DC:
147 return 0;
148 }
149 }
150
151 public isWebSocketConnectionOpened(): boolean {
152 return this?.wsConnection?.readyState === OPEN;
153 }
154
155 public getRegistrationStatus(): RegistrationStatus {
156 return this?.bootNotificationResponse?.status;
157 }
158
159 public isInUnknownState(): boolean {
160 return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
161 }
162
163 public isInPendingState(): boolean {
164 return this?.bootNotificationResponse?.status === RegistrationStatus.PENDING;
165 }
166
167 public isInAcceptedState(): boolean {
168 return this?.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED;
169 }
170
171 public isInRejectedState(): boolean {
172 return this?.bootNotificationResponse?.status === RegistrationStatus.REJECTED;
173 }
174
175 public isRegistered(): boolean {
176 return !this.isInUnknownState() && (this.isInAcceptedState() || this.isInPendingState());
177 }
178
179 public isChargingStationAvailable(): boolean {
180 return this.getConnectorStatus(0).availability === AvailabilityType.OPERATIVE;
181 }
182
183 public isConnectorAvailable(id: number): boolean {
184 return id > 0 && this.getConnectorStatus(id).availability === AvailabilityType.OPERATIVE;
185 }
186
187 public getNumberOfConnectors(): number {
188 return this.connectors.get(0) ? this.connectors.size - 1 : this.connectors.size;
189 }
190
191 public getConnectorStatus(id: number): ConnectorStatus {
192 return this.connectors.get(id);
193 }
194
195 public getCurrentOutType(): CurrentType | undefined {
196 return this.stationInfo.currentOutType ?? CurrentType.AC;
197 }
198
199 public getOcppStrictCompliance(): boolean {
200 return this.stationInfo.ocppStrictCompliance ?? false;
201 }
202
203 public getVoltageOut(): number | undefined {
204 const errMsg = `${this.logPrefix()} Unknown ${this.getCurrentOutType()} currentOutType in template file ${
205 this.stationTemplateFile
206 }, cannot define default voltage out`;
207 let defaultVoltageOut: number;
208 switch (this.getCurrentOutType()) {
209 case CurrentType.AC:
210 defaultVoltageOut = Voltage.VOLTAGE_230;
211 break;
212 case CurrentType.DC:
213 defaultVoltageOut = Voltage.VOLTAGE_400;
214 break;
215 default:
216 logger.error(errMsg);
217 throw new Error(errMsg);
218 }
219 return !Utils.isUndefined(this.stationInfo.voltageOut)
220 ? this.stationInfo.voltageOut
221 : defaultVoltageOut;
222 }
223
224 public getTransactionIdTag(transactionId: number): string | undefined {
225 for (const connectorId of this.connectors.keys()) {
226 if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionId === transactionId) {
227 return this.getConnectorStatus(connectorId).transactionIdTag;
228 }
229 }
230 }
231
232 public getOutOfOrderEndMeterValues(): boolean {
233 return this.stationInfo.outOfOrderEndMeterValues ?? false;
234 }
235
236 public getBeginEndMeterValues(): boolean {
237 return this.stationInfo.beginEndMeterValues ?? false;
238 }
239
240 public getMeteringPerTransaction(): boolean {
241 return this.stationInfo.meteringPerTransaction ?? true;
242 }
243
244 public getTransactionDataMeterValues(): boolean {
245 return this.stationInfo.transactionDataMeterValues ?? false;
246 }
247
248 public getMainVoltageMeterValues(): boolean {
249 return this.stationInfo.mainVoltageMeterValues ?? true;
250 }
251
252 public getPhaseLineToLineVoltageMeterValues(): boolean {
253 return this.stationInfo.phaseLineToLineVoltageMeterValues ?? false;
254 }
255
256 public getEnergyActiveImportRegisterByTransactionId(transactionId: number): number | undefined {
257 if (this.getMeteringPerTransaction()) {
258 for (const connectorId of this.connectors.keys()) {
259 if (
260 connectorId > 0 &&
261 this.getConnectorStatus(connectorId).transactionId === transactionId
262 ) {
263 return this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue;
264 }
265 }
266 }
267 for (const connectorId of this.connectors.keys()) {
268 if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionId === transactionId) {
269 return this.getConnectorStatus(connectorId).energyActiveImportRegisterValue;
270 }
271 }
272 }
273
274 public getEnergyActiveImportRegisterByConnectorId(connectorId: number): number | undefined {
275 if (this.getMeteringPerTransaction()) {
276 return this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue;
277 }
278 return this.getConnectorStatus(connectorId).energyActiveImportRegisterValue;
279 }
280
281 public getAuthorizeRemoteTxRequests(): boolean {
282 const authorizeRemoteTxRequests = this.getConfigurationKey(
283 StandardParametersKey.AuthorizeRemoteTxRequests
284 );
285 return authorizeRemoteTxRequests
286 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
287 : false;
288 }
289
290 public getLocalAuthListEnabled(): boolean {
291 const localAuthListEnabled = this.getConfigurationKey(
292 StandardParametersKey.LocalAuthListEnabled
293 );
294 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
295 }
296
297 public restartWebSocketPing(): void {
298 // Stop WebSocket ping
299 this.stopWebSocketPing();
300 // Start WebSocket ping
301 this.startWebSocketPing();
302 }
303
304 public getSampledValueTemplate(
305 connectorId: number,
306 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
307 phase?: MeterValuePhase
308 ): SampledValueTemplate | undefined {
309 const onPhaseStr = phase ? `on phase ${phase} ` : '';
310 if (!Constants.SUPPORTED_MEASURANDS.includes(measurand)) {
311 logger.warn(
312 `${this.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
313 );
314 return;
315 }
316 if (
317 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
318 !this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(
319 measurand
320 )
321 ) {
322 logger.debug(
323 `${this.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
324 StandardParametersKey.MeterValuesSampledData
325 }' OCPP parameter`
326 );
327 return;
328 }
329 const sampledValueTemplates: SampledValueTemplate[] =
330 this.getConnectorStatus(connectorId).MeterValues;
331 for (
332 let index = 0;
333 !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
334 index++
335 ) {
336 if (
337 !Constants.SUPPORTED_MEASURANDS.includes(
338 sampledValueTemplates[index]?.measurand ??
339 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
340 )
341 ) {
342 logger.warn(
343 `${this.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
344 );
345 } else if (
346 phase &&
347 sampledValueTemplates[index]?.phase === phase &&
348 sampledValueTemplates[index]?.measurand === measurand &&
349 this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(
350 measurand
351 )
352 ) {
353 return sampledValueTemplates[index];
354 } else if (
355 !phase &&
356 !sampledValueTemplates[index].phase &&
357 sampledValueTemplates[index]?.measurand === measurand &&
358 this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(
359 measurand
360 )
361 ) {
362 return sampledValueTemplates[index];
363 } else if (
364 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
365 (!sampledValueTemplates[index].measurand ||
366 sampledValueTemplates[index].measurand === measurand)
367 ) {
368 return sampledValueTemplates[index];
369 }
370 }
371 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
372 const errorMsg = `${this.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
373 logger.error(errorMsg);
374 throw new Error(errorMsg);
375 }
376 logger.debug(
377 `${this.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
378 );
379 }
380
381 public getAutomaticTransactionGeneratorRequireAuthorize(): boolean {
382 return this.stationInfo.AutomaticTransactionGenerator.requireAuthorize ?? true;
383 }
384
385 public startHeartbeat(): void {
386 if (
387 this.getHeartbeatInterval() &&
388 this.getHeartbeatInterval() > 0 &&
389 !this.heartbeatSetInterval
390 ) {
391 // eslint-disable-next-line @typescript-eslint/no-misused-promises
392 this.heartbeatSetInterval = setInterval(async (): Promise<void> => {
393 await this.ocppRequestService.sendHeartbeat();
394 }, this.getHeartbeatInterval());
395 logger.info(
396 this.logPrefix() +
397 ' Heartbeat started every ' +
398 Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
399 );
400 } else if (this.heartbeatSetInterval) {
401 logger.info(
402 this.logPrefix() +
403 ' Heartbeat already started every ' +
404 Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
405 );
406 } else {
407 logger.error(
408 `${this.logPrefix()} Heartbeat interval set to ${
409 this.getHeartbeatInterval()
410 ? Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
411 : this.getHeartbeatInterval()
412 }, not starting the heartbeat`
413 );
414 }
415 }
416
417 public restartHeartbeat(): void {
418 // Stop heartbeat
419 this.stopHeartbeat();
420 // Start heartbeat
421 this.startHeartbeat();
422 }
423
424 public startMeterValues(connectorId: number, interval: number): void {
425 if (connectorId === 0) {
426 logger.error(
427 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
428 );
429 return;
430 }
431 if (!this.getConnectorStatus(connectorId)) {
432 logger.error(
433 `${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`
434 );
435 return;
436 }
437 if (!this.getConnectorStatus(connectorId)?.transactionStarted) {
438 logger.error(
439 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
440 );
441 return;
442 } else if (
443 this.getConnectorStatus(connectorId)?.transactionStarted &&
444 !this.getConnectorStatus(connectorId)?.transactionId
445 ) {
446 logger.error(
447 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`
448 );
449 return;
450 }
451 if (interval > 0) {
452 // eslint-disable-next-line @typescript-eslint/no-misused-promises
453 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(
454 async (): Promise<void> => {
455 await this.ocppRequestService.sendMeterValues(
456 connectorId,
457 this.getConnectorStatus(connectorId).transactionId,
458 interval
459 );
460 },
461 interval
462 );
463 } else {
464 logger.error(
465 `${this.logPrefix()} Charging station ${
466 StandardParametersKey.MeterValueSampleInterval
467 } configuration set to ${
468 interval ? Utils.formatDurationMilliSeconds(interval) : interval
469 }, not sending MeterValues`
470 );
471 }
472 }
473
474 public start(): void {
475 if (this.getEnableStatistics()) {
476 this.performanceStatistics.start();
477 }
478 this.openWSConnection();
479 // Monitor authorization file
480 this.startAuthorizationFileMonitoring();
481 // Monitor station template file
482 this.startStationTemplateFileMonitoring();
483 // Handle WebSocket message
484 this.wsConnection.on('message', this.onMessage.bind(this));
485 // Handle WebSocket error
486 this.wsConnection.on('error', this.onError.bind(this));
487 // Handle WebSocket close
488 this.wsConnection.on('close', this.onClose.bind(this));
489 // Handle WebSocket open
490 this.wsConnection.on('open', this.onOpen.bind(this));
491 // Handle WebSocket ping
492 this.wsConnection.on('ping', this.onPing.bind(this));
493 // Handle WebSocket pong
494 this.wsConnection.on('pong', this.onPong.bind(this));
495 parentPort.postMessage({
496 id: ChargingStationWorkerMessageEvents.STARTED,
497 data: { id: this.stationInfo.chargingStationId },
498 });
499 }
500
501 public async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
502 // Stop message sequence
503 await this.stopMessageSequence(reason);
504 for (const connectorId of this.connectors.keys()) {
505 if (connectorId > 0) {
506 await this.ocppRequestService.sendStatusNotification(
507 connectorId,
508 ChargePointStatus.UNAVAILABLE
509 );
510 this.getConnectorStatus(connectorId).status = ChargePointStatus.UNAVAILABLE;
511 }
512 }
513 if (this.isWebSocketConnectionOpened()) {
514 this.wsConnection.close();
515 }
516 if (this.getEnableStatistics()) {
517 this.performanceStatistics.stop();
518 }
519 this.bootNotificationResponse = null;
520 parentPort.postMessage({
521 id: ChargingStationWorkerMessageEvents.STOPPED,
522 data: { id: this.stationInfo.chargingStationId },
523 });
524 this.stopped = true;
525 }
526
527 public getConfigurationKey(
528 key: string | StandardParametersKey,
529 caseInsensitive = false
530 ): ConfigurationKey | undefined {
531 return this.configuration.configurationKey.find((configElement) => {
532 if (caseInsensitive) {
533 return configElement.key.toLowerCase() === key.toLowerCase();
534 }
535 return configElement.key === key;
536 });
537 }
538
539 public addConfigurationKey(
540 key: string | StandardParametersKey,
541 value: string,
542 options: { readonly?: boolean; visible?: boolean; reboot?: boolean } = {
543 readonly: false,
544 visible: true,
545 reboot: false,
546 }
547 ): void {
548 const keyFound = this.getConfigurationKey(key);
549 const readonly = options.readonly;
550 const visible = options.visible;
551 const reboot = options.reboot;
552 if (!keyFound) {
553 this.configuration.configurationKey.push({
554 key,
555 readonly,
556 value,
557 visible,
558 reboot,
559 });
560 } else {
561 logger.error(
562 `${this.logPrefix()} Trying to add an already existing configuration key: %j`,
563 keyFound
564 );
565 }
566 }
567
568 public setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
569 const keyFound = this.getConfigurationKey(key);
570 if (keyFound) {
571 const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
572 this.configuration.configurationKey[keyIndex].value = value;
573 } else {
574 logger.error(
575 `${this.logPrefix()} Trying to set a value on a non existing configuration key: %j`,
576 { key, value }
577 );
578 }
579 }
580
581 public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
582 let cpReplaced = false;
583 if (!Utils.isEmptyArray(this.getConnectorStatus(connectorId).chargingProfiles)) {
584 this.getConnectorStatus(connectorId).chargingProfiles?.forEach(
585 (chargingProfile: ChargingProfile, index: number) => {
586 if (
587 chargingProfile.chargingProfileId === cp.chargingProfileId ||
588 (chargingProfile.stackLevel === cp.stackLevel &&
589 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
590 ) {
591 this.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
592 cpReplaced = true;
593 }
594 }
595 );
596 }
597 !cpReplaced && this.getConnectorStatus(connectorId).chargingProfiles?.push(cp);
598 }
599
600 public resetConnectorStatus(connectorId: number): void {
601 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
602 this.getConnectorStatus(connectorId).idTagAuthorized = false;
603 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
604 this.getConnectorStatus(connectorId).transactionStarted = false;
605 delete this.getConnectorStatus(connectorId).localAuthorizeIdTag;
606 delete this.getConnectorStatus(connectorId).authorizeIdTag;
607 delete this.getConnectorStatus(connectorId).transactionId;
608 delete this.getConnectorStatus(connectorId).transactionIdTag;
609 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
610 delete this.getConnectorStatus(connectorId).transactionBeginMeterValue;
611 this.stopMeterValues(connectorId);
612 }
613
614 public bufferMessage(message: string): void {
615 this.messageBuffer.add(message);
616 }
617
618 private flushMessageBuffer() {
619 if (this.messageBuffer.size > 0) {
620 this.messageBuffer.forEach((message) => {
621 // TODO: evaluate the need to track performance
622 this.wsConnection.send(message);
623 this.messageBuffer.delete(message);
624 });
625 }
626 }
627
628 private getSupervisionUrlOcppConfiguration(): boolean {
629 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
630 }
631
632 private getChargingStationId(stationTemplate: ChargingStationTemplate): string {
633 // In case of multiple instances: add instance index to charging station id
634 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
635 const idSuffix = stationTemplate.nameSuffix ?? '';
636 return stationTemplate.fixedName
637 ? stationTemplate.baseName
638 : stationTemplate.baseName +
639 '-' +
640 instanceIndex.toString() +
641 ('000000000' + this.index.toString()).substr(
642 ('000000000' + this.index.toString()).length - 4
643 ) +
644 idSuffix;
645 }
646
647 private buildStationInfo(): ChargingStationInfo {
648 let stationTemplateFromFile: ChargingStationTemplate;
649 try {
650 // Load template file
651 const fileDescriptor = fs.openSync(this.stationTemplateFile, 'r');
652 stationTemplateFromFile = JSON.parse(
653 fs.readFileSync(fileDescriptor, 'utf8')
654 ) as ChargingStationTemplate;
655 fs.closeSync(fileDescriptor);
656 } catch (error) {
657 FileUtils.handleFileException(
658 this.logPrefix(),
659 'Template',
660 this.stationTemplateFile,
661 error as NodeJS.ErrnoException
662 );
663 }
664 const chargingStationId = this.getChargingStationId(stationTemplateFromFile);
665 // Deprecation template keys section
666 this.warnDeprecatedTemplateKey(
667 stationTemplateFromFile,
668 'supervisionUrl',
669 chargingStationId,
670 "Use 'supervisionUrls' instead"
671 );
672 this.convertDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', 'supervisionUrls');
673 const stationInfo: ChargingStationInfo = stationTemplateFromFile ?? ({} as ChargingStationInfo);
674 stationInfo.wsOptions = stationTemplateFromFile?.wsOptions ?? {};
675 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
676 stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
677 const powerArrayRandomIndex = Math.floor(
678 Utils.secureRandom() * stationTemplateFromFile.power.length
679 );
680 stationInfo.maxPower =
681 stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
682 ? stationTemplateFromFile.power[powerArrayRandomIndex] * 1000
683 : stationTemplateFromFile.power[powerArrayRandomIndex];
684 } else {
685 stationTemplateFromFile.power = stationTemplateFromFile.power as number;
686 stationInfo.maxPower =
687 stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
688 ? stationTemplateFromFile.power * 1000
689 : stationTemplateFromFile.power;
690 }
691 delete stationInfo.power;
692 delete stationInfo.powerUnit;
693 stationInfo.chargingStationId = chargingStationId;
694 stationInfo.resetTime = stationTemplateFromFile.resetTime
695 ? stationTemplateFromFile.resetTime * 1000
696 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
697 return stationInfo;
698 }
699
700 private getOcppVersion(): OCPPVersion {
701 return this.stationInfo.ocppVersion ? this.stationInfo.ocppVersion : OCPPVersion.VERSION_16;
702 }
703
704 private handleUnsupportedVersion(version: OCPPVersion) {
705 const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${
706 this.stationTemplateFile
707 }`;
708 logger.error(errMsg);
709 throw new Error(errMsg);
710 }
711
712 private initialize(): void {
713 this.stationInfo = this.buildStationInfo();
714 this.configuration = this.getTemplateChargingStationConfiguration();
715 delete this.stationInfo.Configuration;
716 this.bootNotificationRequest = {
717 chargePointModel: this.stationInfo.chargePointModel,
718 chargePointVendor: this.stationInfo.chargePointVendor,
719 ...(!Utils.isUndefined(this.stationInfo.chargeBoxSerialNumberPrefix) && {
720 chargeBoxSerialNumber: this.stationInfo.chargeBoxSerialNumberPrefix,
721 }),
722 ...(!Utils.isUndefined(this.stationInfo.firmwareVersion) && {
723 firmwareVersion: this.stationInfo.firmwareVersion,
724 }),
725 };
726 // Build connectors if needed
727 const maxConnectors = this.getMaxNumberOfConnectors();
728 if (maxConnectors <= 0) {
729 logger.warn(
730 `${this.logPrefix()} Charging station template ${
731 this.stationTemplateFile
732 } with ${maxConnectors} connectors`
733 );
734 }
735 const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors();
736 if (templateMaxConnectors <= 0) {
737 logger.warn(
738 `${this.logPrefix()} Charging station template ${
739 this.stationTemplateFile
740 } with no connector configuration`
741 );
742 }
743 if (!this.stationInfo.Connectors[0]) {
744 logger.warn(
745 `${this.logPrefix()} Charging station template ${
746 this.stationTemplateFile
747 } with no connector Id 0 configuration`
748 );
749 }
750 // Sanity check
751 if (
752 maxConnectors >
753 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
754 !this.stationInfo.randomConnectors
755 ) {
756 logger.warn(
757 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
758 this.stationTemplateFile
759 }, forcing random connector configurations affectation`
760 );
761 this.stationInfo.randomConnectors = true;
762 }
763 const connectorsConfigHash = crypto
764 .createHash('sha256')
765 .update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString())
766 .digest('hex');
767 const connectorsConfigChanged =
768 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
769 if (this.connectors?.size === 0 || connectorsConfigChanged) {
770 connectorsConfigChanged && this.connectors.clear();
771 this.connectorsConfigurationHash = connectorsConfigHash;
772 // Add connector Id 0
773 let lastConnector = '0';
774 for (lastConnector in this.stationInfo.Connectors) {
775 const lastConnectorId = Utils.convertToInt(lastConnector);
776 if (
777 lastConnectorId === 0 &&
778 this.getUseConnectorId0() &&
779 this.stationInfo.Connectors[lastConnector]
780 ) {
781 this.connectors.set(
782 lastConnectorId,
783 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[lastConnector])
784 );
785 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
786 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
787 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
788 }
789 }
790 }
791 // Generate all connectors
792 if (
793 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0
794 ) {
795 for (let index = 1; index <= maxConnectors; index++) {
796 const randConnectorId = this.stationInfo.randomConnectors
797 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
798 : index;
799 this.connectors.set(
800 index,
801 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[randConnectorId])
802 );
803 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
804 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
805 this.getConnectorStatus(index).chargingProfiles = [];
806 }
807 }
808 }
809 }
810 // Avoid duplication of connectors related information
811 delete this.stationInfo.Connectors;
812 // Initialize transaction attributes on connectors
813 for (const connectorId of this.connectors.keys()) {
814 if (connectorId > 0 && !this.getConnectorStatus(connectorId)?.transactionStarted) {
815 this.initializeConnectorStatus(connectorId);
816 }
817 }
818 this.wsConfiguredConnectionUrl = new URL(
819 this.getConfiguredSupervisionUrl().href + '/' + this.stationInfo.chargingStationId
820 );
821 switch (this.getOcppVersion()) {
822 case OCPPVersion.VERSION_16:
823 this.ocppIncomingRequestService =
824 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>(this);
825 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
826 this,
827 OCPP16ResponseService.getInstance<OCPP16ResponseService>(this)
828 );
829 break;
830 default:
831 this.handleUnsupportedVersion(this.getOcppVersion());
832 break;
833 }
834 // OCPP parameters
835 this.initOcppParameters();
836 if (this.stationInfo.autoRegister) {
837 this.bootNotificationResponse = {
838 currentTime: new Date().toISOString(),
839 interval: this.getHeartbeatInterval() / 1000,
840 status: RegistrationStatus.ACCEPTED,
841 };
842 }
843 this.stationInfo.powerDivider = this.getPowerDivider();
844 if (this.getEnableStatistics()) {
845 this.performanceStatistics = PerformanceStatistics.getInstance(
846 this.id,
847 this.stationInfo.chargingStationId,
848 this.wsConnectionUrl
849 );
850 }
851 }
852
853 private initOcppParameters(): void {
854 if (
855 this.getSupervisionUrlOcppConfiguration() &&
856 !this.getConfigurationKey(
857 this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl
858 )
859 ) {
860 this.addConfigurationKey(
861 VendorDefaultParametersKey.ConnectionUrl,
862 this.getConfiguredSupervisionUrl().href,
863 { reboot: true }
864 );
865 }
866 if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) {
867 this.addConfigurationKey(
868 StandardParametersKey.SupportedFeatureProfiles,
869 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.Local_Auth_List_Management},${SupportedFeatureProfiles.Smart_Charging}`
870 );
871 }
872 this.addConfigurationKey(
873 StandardParametersKey.NumberOfConnectors,
874 this.getNumberOfConnectors().toString(),
875 { readonly: true }
876 );
877 if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
878 this.addConfigurationKey(
879 StandardParametersKey.MeterValuesSampledData,
880 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
881 );
882 }
883 if (!this.getConfigurationKey(StandardParametersKey.ConnectorPhaseRotation)) {
884 const connectorPhaseRotation = [];
885 for (const connectorId of this.connectors.keys()) {
886 // AC/DC
887 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
888 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
889 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
890 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
891 // AC
892 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
893 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
894 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
895 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
896 }
897 }
898 this.addConfigurationKey(
899 StandardParametersKey.ConnectorPhaseRotation,
900 connectorPhaseRotation.toString()
901 );
902 }
903 if (!this.getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests)) {
904 this.addConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
905 }
906 if (
907 !this.getConfigurationKey(StandardParametersKey.LocalAuthListEnabled) &&
908 this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles).value.includes(
909 SupportedFeatureProfiles.Local_Auth_List_Management
910 )
911 ) {
912 this.addConfigurationKey(StandardParametersKey.LocalAuthListEnabled, 'false');
913 }
914 if (!this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
915 this.addConfigurationKey(
916 StandardParametersKey.ConnectionTimeOut,
917 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
918 );
919 }
920 }
921
922 private async onOpen(): Promise<void> {
923 logger.info(
924 `${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`
925 );
926 if (!this.isInAcceptedState()) {
927 // Send BootNotification
928 let registrationRetryCount = 0;
929 do {
930 this.bootNotificationResponse = await this.ocppRequestService.sendBootNotification(
931 this.bootNotificationRequest.chargePointModel,
932 this.bootNotificationRequest.chargePointVendor,
933 this.bootNotificationRequest.chargeBoxSerialNumber,
934 this.bootNotificationRequest.firmwareVersion
935 );
936 if (!this.isInAcceptedState()) {
937 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
938 await Utils.sleep(
939 this.bootNotificationResponse?.interval
940 ? this.bootNotificationResponse.interval * 1000
941 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
942 );
943 }
944 } while (
945 !this.isInAcceptedState() &&
946 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
947 this.getRegistrationMaxRetries() === -1)
948 );
949 }
950 if (this.isInAcceptedState()) {
951 await this.startMessageSequence();
952 this.stopped && (this.stopped = false);
953 if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) {
954 this.flushMessageBuffer();
955 }
956 } else {
957 logger.error(
958 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
959 );
960 }
961 this.autoReconnectRetryCount = 0;
962 this.wsConnectionRestarted = false;
963 }
964
965 private async onClose(code: number, reason: string): Promise<void> {
966 switch (code) {
967 // Normal close
968 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
969 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
970 logger.info(
971 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
972 code
973 )}' and reason '${reason}'`
974 );
975 this.autoReconnectRetryCount = 0;
976 break;
977 // Abnormal close
978 default:
979 logger.error(
980 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
981 code
982 )}' and reason '${reason}'`
983 );
984 await this.reconnect(code);
985 break;
986 }
987 }
988
989 private async onMessage(data: Data): Promise<void> {
990 let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [
991 0,
992 '',
993 '' as IncomingRequestCommand,
994 {},
995 {},
996 ];
997 let responseCallback: (
998 payload: JsonType | string,
999 requestPayload: JsonType | OCPPError
1000 ) => void;
1001 let rejectCallback: (error: OCPPError, requestStatistic?: boolean) => void;
1002 let requestCommandName: RequestCommand | IncomingRequestCommand;
1003 let requestPayload: JsonType | OCPPError;
1004 let cachedRequest: CachedRequest;
1005 let errMsg: string;
1006 try {
1007 const request = JSON.parse(data.toString()) as IncomingRequest;
1008 if (Utils.isIterable(request)) {
1009 // Parse the message
1010 [messageType, messageId, commandName, commandPayload, errorDetails] = request;
1011 } else {
1012 throw new OCPPError(
1013 ErrorType.PROTOCOL_ERROR,
1014 'Incoming request is not iterable',
1015 commandName
1016 );
1017 }
1018 // Check the Type of message
1019 switch (messageType) {
1020 // Incoming Message
1021 case MessageType.CALL_MESSAGE:
1022 if (this.getEnableStatistics()) {
1023 this.performanceStatistics.addRequestStatistic(commandName, messageType);
1024 }
1025 // Process the call
1026 await this.ocppIncomingRequestService.handleRequest(
1027 messageId,
1028 commandName,
1029 commandPayload
1030 );
1031 break;
1032 // Outcome Message
1033 case MessageType.CALL_RESULT_MESSAGE:
1034 // Respond
1035 cachedRequest = this.requests.get(messageId);
1036 if (Utils.isIterable(cachedRequest)) {
1037 [responseCallback, , , requestPayload] = cachedRequest;
1038 } else {
1039 throw new OCPPError(
1040 ErrorType.PROTOCOL_ERROR,
1041 `Cached request for message id ${messageId} response is not iterable`,
1042 commandName
1043 );
1044 }
1045 if (!responseCallback) {
1046 // Error
1047 throw new OCPPError(
1048 ErrorType.INTERNAL_ERROR,
1049 `Response for unknown message id ${messageId}`,
1050 commandName
1051 );
1052 }
1053 responseCallback(commandName, requestPayload);
1054 break;
1055 // Error Message
1056 case MessageType.CALL_ERROR_MESSAGE:
1057 cachedRequest = this.requests.get(messageId);
1058 if (Utils.isIterable(cachedRequest)) {
1059 [, rejectCallback, requestCommandName] = cachedRequest;
1060 } else {
1061 throw new OCPPError(
1062 ErrorType.PROTOCOL_ERROR,
1063 `Cached request for message id ${messageId} error response is not iterable`
1064 );
1065 }
1066 if (!rejectCallback) {
1067 // Error
1068 throw new OCPPError(
1069 ErrorType.INTERNAL_ERROR,
1070 `Error response for unknown message id ${messageId}`,
1071 requestCommandName
1072 );
1073 }
1074 rejectCallback(
1075 new OCPPError(commandName, commandPayload.toString(), requestCommandName, errorDetails)
1076 );
1077 break;
1078 // Error
1079 default:
1080 errMsg = `${this.logPrefix()} Wrong message type ${messageType}`;
1081 logger.error(errMsg);
1082 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1083 }
1084 } catch (error) {
1085 // Log
1086 logger.error(
1087 '%s Incoming OCPP message %j matching cached request %j processing error %j',
1088 this.logPrefix(),
1089 data.toString(),
1090 this.requests.get(messageId),
1091 error
1092 );
1093 // Send error
1094 messageType === MessageType.CALL_MESSAGE &&
1095 (await this.ocppRequestService.sendError(messageId, error as OCPPError, commandName));
1096 }
1097 }
1098
1099 private onPing(): void {
1100 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
1101 }
1102
1103 private onPong(): void {
1104 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
1105 }
1106
1107 private async onError(error: WSError): Promise<void> {
1108 logger.error(this.logPrefix() + ' WebSocket error: %j', error);
1109 // switch (error.code) {
1110 // case 'ECONNREFUSED':
1111 // await this.reconnect(error);
1112 // break;
1113 // }
1114 }
1115
1116 private getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
1117 return this.stationInfo.Configuration ?? ({} as ChargingStationConfiguration);
1118 }
1119
1120 private getAuthorizationFile(): string | undefined {
1121 return (
1122 this.stationInfo.authorizationFile &&
1123 path.join(
1124 path.resolve(__dirname, '../'),
1125 'assets',
1126 path.basename(this.stationInfo.authorizationFile)
1127 )
1128 );
1129 }
1130
1131 private getAuthorizedTags(): string[] {
1132 let authorizedTags: string[] = [];
1133 const authorizationFile = this.getAuthorizationFile();
1134 if (authorizationFile) {
1135 try {
1136 // Load authorization file
1137 const fileDescriptor = fs.openSync(authorizationFile, 'r');
1138 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
1139 fs.closeSync(fileDescriptor);
1140 } catch (error) {
1141 FileUtils.handleFileException(
1142 this.logPrefix(),
1143 'Authorization',
1144 authorizationFile,
1145 error as NodeJS.ErrnoException
1146 );
1147 }
1148 } else {
1149 logger.info(
1150 this.logPrefix() +
1151 ' No authorization file given in template file ' +
1152 this.stationTemplateFile
1153 );
1154 }
1155 return authorizedTags;
1156 }
1157
1158 private getUseConnectorId0(): boolean | undefined {
1159 return !Utils.isUndefined(this.stationInfo.useConnectorId0)
1160 ? this.stationInfo.useConnectorId0
1161 : true;
1162 }
1163
1164 private getNumberOfRunningTransactions(): number {
1165 let trxCount = 0;
1166 for (const connectorId of this.connectors.keys()) {
1167 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1168 trxCount++;
1169 }
1170 }
1171 return trxCount;
1172 }
1173
1174 // 0 for disabling
1175 private getConnectionTimeout(): number | undefined {
1176 if (this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
1177 return (
1178 parseInt(this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut).value) ??
1179 Constants.DEFAULT_CONNECTION_TIMEOUT
1180 );
1181 }
1182 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1183 }
1184
1185 // -1 for unlimited, 0 for disabling
1186 private getAutoReconnectMaxRetries(): number | undefined {
1187 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1188 return this.stationInfo.autoReconnectMaxRetries;
1189 }
1190 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1191 return Configuration.getAutoReconnectMaxRetries();
1192 }
1193 return -1;
1194 }
1195
1196 // 0 for disabling
1197 private getRegistrationMaxRetries(): number | undefined {
1198 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1199 return this.stationInfo.registrationMaxRetries;
1200 }
1201 return -1;
1202 }
1203
1204 private getPowerDivider(): number {
1205 let powerDivider = this.getNumberOfConnectors();
1206 if (this.stationInfo.powerSharedByConnectors) {
1207 powerDivider = this.getNumberOfRunningTransactions();
1208 }
1209 return powerDivider;
1210 }
1211
1212 private getTemplateMaxNumberOfConnectors(): number {
1213 return Object.keys(this.stationInfo.Connectors).length;
1214 }
1215
1216 private getMaxNumberOfConnectors(): number {
1217 let maxConnectors: number;
1218 if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) {
1219 const numberOfConnectors = this.stationInfo.numberOfConnectors as number[];
1220 // Distribute evenly the number of connectors
1221 maxConnectors = numberOfConnectors[(this.index - 1) % numberOfConnectors.length];
1222 } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) {
1223 maxConnectors = this.stationInfo.numberOfConnectors as number;
1224 } else {
1225 maxConnectors = this.stationInfo.Connectors[0]
1226 ? this.getTemplateMaxNumberOfConnectors() - 1
1227 : this.getTemplateMaxNumberOfConnectors();
1228 }
1229 return maxConnectors;
1230 }
1231
1232 private async startMessageSequence(): Promise<void> {
1233 if (this.stationInfo.autoRegister) {
1234 await this.ocppRequestService.sendBootNotification(
1235 this.bootNotificationRequest.chargePointModel,
1236 this.bootNotificationRequest.chargePointVendor,
1237 this.bootNotificationRequest.chargeBoxSerialNumber,
1238 this.bootNotificationRequest.firmwareVersion
1239 );
1240 }
1241 // Start WebSocket ping
1242 this.startWebSocketPing();
1243 // Start heartbeat
1244 this.startHeartbeat();
1245 // Initialize connectors status
1246 for (const connectorId of this.connectors.keys()) {
1247 if (connectorId === 0) {
1248 continue;
1249 } else if (
1250 !this.stopped &&
1251 !this.getConnectorStatus(connectorId)?.status &&
1252 this.getConnectorStatus(connectorId)?.bootStatus
1253 ) {
1254 // Send status in template at startup
1255 await this.ocppRequestService.sendStatusNotification(
1256 connectorId,
1257 this.getConnectorStatus(connectorId).bootStatus
1258 );
1259 this.getConnectorStatus(connectorId).status =
1260 this.getConnectorStatus(connectorId).bootStatus;
1261 } else if (
1262 this.stopped &&
1263 this.getConnectorStatus(connectorId)?.status &&
1264 this.getConnectorStatus(connectorId)?.bootStatus
1265 ) {
1266 // Send status in template after reset
1267 await this.ocppRequestService.sendStatusNotification(
1268 connectorId,
1269 this.getConnectorStatus(connectorId).bootStatus
1270 );
1271 this.getConnectorStatus(connectorId).status =
1272 this.getConnectorStatus(connectorId).bootStatus;
1273 } else if (!this.stopped && this.getConnectorStatus(connectorId)?.status) {
1274 // Send previous status at template reload
1275 await this.ocppRequestService.sendStatusNotification(
1276 connectorId,
1277 this.getConnectorStatus(connectorId).status
1278 );
1279 } else {
1280 // Send default status
1281 await this.ocppRequestService.sendStatusNotification(
1282 connectorId,
1283 ChargePointStatus.AVAILABLE
1284 );
1285 this.getConnectorStatus(connectorId).status = ChargePointStatus.AVAILABLE;
1286 }
1287 }
1288 // Start the ATG
1289 this.startAutomaticTransactionGenerator();
1290 }
1291
1292 private startAutomaticTransactionGenerator() {
1293 if (this.stationInfo.AutomaticTransactionGenerator.enable) {
1294 if (!this.automaticTransactionGenerator) {
1295 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
1296 }
1297 if (!this.automaticTransactionGenerator.started) {
1298 this.automaticTransactionGenerator.start();
1299 }
1300 }
1301 }
1302
1303 private async stopMessageSequence(
1304 reason: StopTransactionReason = StopTransactionReason.NONE
1305 ): Promise<void> {
1306 // Stop WebSocket ping
1307 this.stopWebSocketPing();
1308 // Stop heartbeat
1309 this.stopHeartbeat();
1310 // Stop the ATG
1311 if (
1312 this.stationInfo.AutomaticTransactionGenerator.enable &&
1313 this.automaticTransactionGenerator?.started
1314 ) {
1315 this.automaticTransactionGenerator.stop();
1316 } else {
1317 for (const connectorId of this.connectors.keys()) {
1318 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1319 const transactionId = this.getConnectorStatus(connectorId).transactionId;
1320 await this.ocppRequestService.sendStopTransaction(
1321 transactionId,
1322 this.getEnergyActiveImportRegisterByTransactionId(transactionId),
1323 this.getTransactionIdTag(transactionId),
1324 reason
1325 );
1326 }
1327 }
1328 }
1329 }
1330
1331 private startWebSocketPing(): void {
1332 const webSocketPingInterval: number = this.getConfigurationKey(
1333 StandardParametersKey.WebSocketPingInterval
1334 )
1335 ? Utils.convertToInt(
1336 this.getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value
1337 )
1338 : 0;
1339 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1340 this.webSocketPingSetInterval = setInterval(() => {
1341 if (this.isWebSocketConnectionOpened()) {
1342 this.wsConnection.ping((): void => {
1343 /* This is intentional */
1344 });
1345 }
1346 }, webSocketPingInterval * 1000);
1347 logger.info(
1348 this.logPrefix() +
1349 ' WebSocket ping started every ' +
1350 Utils.formatDurationSeconds(webSocketPingInterval)
1351 );
1352 } else if (this.webSocketPingSetInterval) {
1353 logger.info(
1354 this.logPrefix() +
1355 ' WebSocket ping every ' +
1356 Utils.formatDurationSeconds(webSocketPingInterval) +
1357 ' already started'
1358 );
1359 } else {
1360 logger.error(
1361 `${this.logPrefix()} WebSocket ping interval set to ${
1362 webSocketPingInterval
1363 ? Utils.formatDurationSeconds(webSocketPingInterval)
1364 : webSocketPingInterval
1365 }, not starting the WebSocket ping`
1366 );
1367 }
1368 }
1369
1370 private stopWebSocketPing(): void {
1371 if (this.webSocketPingSetInterval) {
1372 clearInterval(this.webSocketPingSetInterval);
1373 }
1374 }
1375
1376 private warnDeprecatedTemplateKey(
1377 template: ChargingStationTemplate,
1378 key: string,
1379 chargingStationId: string,
1380 logMsgToAppend = ''
1381 ): void {
1382 if (!Utils.isUndefined(template[key])) {
1383 const logPrefixStr = ` ${chargingStationId} |`;
1384 logger.warn(
1385 `${Utils.logPrefix(logPrefixStr)} Deprecated template key '${key}' usage in file '${
1386 this.stationTemplateFile
1387 }'${logMsgToAppend && '. ' + logMsgToAppend}`
1388 );
1389 }
1390 }
1391
1392 private convertDeprecatedTemplateKey(
1393 template: ChargingStationTemplate,
1394 deprecatedKey: string,
1395 key: string
1396 ): void {
1397 if (!Utils.isUndefined(template[deprecatedKey])) {
1398 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1399 template[key] = template[deprecatedKey];
1400 delete template[deprecatedKey];
1401 }
1402 }
1403
1404 private getConfiguredSupervisionUrl(): URL {
1405 const supervisionUrls = Utils.cloneObject<string | string[]>(
1406 this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
1407 );
1408 if (!Utils.isEmptyArray(supervisionUrls)) {
1409 let urlIndex = 0;
1410 switch (Configuration.getSupervisionUrlDistribution()) {
1411 case SupervisionUrlDistribution.ROUND_ROBIN:
1412 urlIndex = (this.index - 1) % supervisionUrls.length;
1413 break;
1414 case SupervisionUrlDistribution.RANDOM:
1415 // Get a random url
1416 urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1417 break;
1418 case SupervisionUrlDistribution.SEQUENTIAL:
1419 if (this.index <= supervisionUrls.length) {
1420 urlIndex = this.index - 1;
1421 } else {
1422 logger.warn(
1423 `${this.logPrefix()} No more configured supervision urls available, using the first one`
1424 );
1425 }
1426 break;
1427 default:
1428 logger.error(
1429 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1430 SupervisionUrlDistribution.ROUND_ROBIN
1431 }`
1432 );
1433 urlIndex = (this.index - 1) % supervisionUrls.length;
1434 break;
1435 }
1436 return new URL(supervisionUrls[urlIndex]);
1437 }
1438 return new URL(supervisionUrls as string);
1439 }
1440
1441 private getHeartbeatInterval(): number | undefined {
1442 const HeartbeatInterval = this.getConfigurationKey(StandardParametersKey.HeartbeatInterval);
1443 if (HeartbeatInterval) {
1444 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1445 }
1446 const HeartBeatInterval = this.getConfigurationKey(StandardParametersKey.HeartBeatInterval);
1447 if (HeartBeatInterval) {
1448 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
1449 }
1450 !this.stationInfo.autoRegister &&
1451 logger.warn(
1452 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1453 Constants.DEFAULT_HEARTBEAT_INTERVAL
1454 }`
1455 );
1456 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
1457 }
1458
1459 private stopHeartbeat(): void {
1460 if (this.heartbeatSetInterval) {
1461 clearInterval(this.heartbeatSetInterval);
1462 }
1463 }
1464
1465 private openWSConnection(
1466 options: ClientOptions & ClientRequestArgs = this.stationInfo.wsOptions,
1467 forceCloseOpened = false
1468 ): void {
1469 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
1470 if (
1471 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
1472 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
1473 ) {
1474 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
1475 }
1476 if (this.isWebSocketConnectionOpened() && forceCloseOpened) {
1477 this.wsConnection.close();
1478 }
1479 let protocol: string;
1480 switch (this.getOcppVersion()) {
1481 case OCPPVersion.VERSION_16:
1482 protocol = 'ocpp' + OCPPVersion.VERSION_16;
1483 break;
1484 default:
1485 this.handleUnsupportedVersion(this.getOcppVersion());
1486 break;
1487 }
1488 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
1489 logger.info(
1490 this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl.toString()
1491 );
1492 }
1493
1494 private stopMeterValues(connectorId: number) {
1495 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1496 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
1497 }
1498 }
1499
1500 private startAuthorizationFileMonitoring(): void {
1501 const authorizationFile = this.getAuthorizationFile();
1502 if (authorizationFile) {
1503 try {
1504 fs.watch(authorizationFile, (event, filename) => {
1505 if (filename && event === 'change') {
1506 try {
1507 logger.debug(
1508 this.logPrefix() +
1509 ' Authorization file ' +
1510 authorizationFile +
1511 ' have changed, reload'
1512 );
1513 // Initialize authorizedTags
1514 this.authorizedTags = this.getAuthorizedTags();
1515 } catch (error) {
1516 logger.error(this.logPrefix() + ' Authorization file monitoring error: %j', error);
1517 }
1518 }
1519 });
1520 } catch (error) {
1521 FileUtils.handleFileException(
1522 this.logPrefix(),
1523 'Authorization',
1524 authorizationFile,
1525 error as NodeJS.ErrnoException
1526 );
1527 }
1528 } else {
1529 logger.info(
1530 this.logPrefix() +
1531 ' No authorization file given in template file ' +
1532 this.stationTemplateFile +
1533 '. Not monitoring changes'
1534 );
1535 }
1536 }
1537
1538 private startStationTemplateFileMonitoring(): void {
1539 try {
1540 fs.watch(this.stationTemplateFile, (event, filename): void => {
1541 if (filename && event === 'change') {
1542 try {
1543 logger.debug(
1544 this.logPrefix() +
1545 ' Template file ' +
1546 this.stationTemplateFile +
1547 ' have changed, reload'
1548 );
1549 // Initialize
1550 this.initialize();
1551 // Restart the ATG
1552 if (
1553 !this.stationInfo.AutomaticTransactionGenerator.enable &&
1554 this.automaticTransactionGenerator
1555 ) {
1556 this.automaticTransactionGenerator.stop();
1557 }
1558 this.startAutomaticTransactionGenerator();
1559 if (this.getEnableStatistics()) {
1560 this.performanceStatistics.restart();
1561 } else {
1562 this.performanceStatistics.stop();
1563 }
1564 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
1565 } catch (error) {
1566 logger.error(
1567 this.logPrefix() + ' Charging station template file monitoring error: %j',
1568 error
1569 );
1570 }
1571 }
1572 });
1573 } catch (error) {
1574 FileUtils.handleFileException(
1575 this.logPrefix(),
1576 'Template',
1577 this.stationTemplateFile,
1578 error as NodeJS.ErrnoException
1579 );
1580 }
1581 }
1582
1583 private getReconnectExponentialDelay(): boolean | undefined {
1584 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
1585 ? this.stationInfo.reconnectExponentialDelay
1586 : false;
1587 }
1588
1589 private async reconnect(code: number): Promise<void> {
1590 // Stop WebSocket ping
1591 this.stopWebSocketPing();
1592 // Stop heartbeat
1593 this.stopHeartbeat();
1594 // Stop the ATG if needed
1595 if (
1596 this.stationInfo.AutomaticTransactionGenerator.enable &&
1597 this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
1598 this.automaticTransactionGenerator?.started
1599 ) {
1600 this.automaticTransactionGenerator.stop();
1601 }
1602 if (
1603 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
1604 this.getAutoReconnectMaxRetries() === -1
1605 ) {
1606 this.autoReconnectRetryCount++;
1607 const reconnectDelay = this.getReconnectExponentialDelay()
1608 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
1609 : this.getConnectionTimeout() * 1000;
1610 const reconnectTimeout = reconnectDelay - 100 > 0 && reconnectDelay;
1611 logger.error(
1612 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
1613 reconnectDelay,
1614 2
1615 )}ms, timeout ${reconnectTimeout}ms`
1616 );
1617 await Utils.sleep(reconnectDelay);
1618 logger.error(
1619 this.logPrefix() +
1620 ' WebSocket: reconnecting try #' +
1621 this.autoReconnectRetryCount.toString()
1622 );
1623 this.openWSConnection(
1624 { ...this.stationInfo.wsOptions, handshakeTimeout: reconnectTimeout },
1625 true
1626 );
1627 this.wsConnectionRestarted = true;
1628 } else if (this.getAutoReconnectMaxRetries() !== -1) {
1629 logger.error(
1630 `${this.logPrefix()} WebSocket reconnect failure: max retries reached (${
1631 this.autoReconnectRetryCount
1632 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
1633 );
1634 }
1635 }
1636
1637 private initializeConnectorStatus(connectorId: number): void {
1638 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
1639 this.getConnectorStatus(connectorId).idTagAuthorized = false;
1640 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
1641 this.getConnectorStatus(connectorId).transactionStarted = false;
1642 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
1643 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
1644 }
1645 }