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