ebd77f83ac54a4d5303712ffcd69a5f78705e81c
[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 { MeterValue, 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 // FIXME: Implement OCPP version agnostic helpers
459 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
460 this,
461 connectorId,
462 this.getConnectorStatus(connectorId).transactionId,
463 interval
464 );
465 await this.ocppRequestService.sendMessageHandler(RequestCommand.METER_VALUES, {
466 connectorId,
467 transactionId: this.getConnectorStatus(connectorId).transactionId,
468 meterValue: [meterValue],
469 });
470 },
471 interval
472 );
473 } else {
474 logger.error(
475 `${this.logPrefix()} Charging station ${
476 StandardParametersKey.MeterValueSampleInterval
477 } configuration set to ${
478 interval ? Utils.formatDurationMilliSeconds(interval) : interval
479 }, not sending MeterValues`
480 );
481 }
482 }
483
484 public start(): void {
485 if (this.getEnableStatistics()) {
486 this.performanceStatistics.start();
487 }
488 this.openWSConnection();
489 // Monitor authorization file
490 this.startAuthorizationFileMonitoring();
491 // Monitor station template file
492 this.startStationTemplateFileMonitoring();
493 // Handle WebSocket message
494 this.wsConnection.on(
495 'message',
496 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
497 );
498 // Handle WebSocket error
499 this.wsConnection.on(
500 'error',
501 this.onError.bind(this) as (this: WebSocket, error: Error) => void
502 );
503 // Handle WebSocket close
504 this.wsConnection.on(
505 'close',
506 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
507 );
508 // Handle WebSocket open
509 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
510 // Handle WebSocket ping
511 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
512 // Handle WebSocket pong
513 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
514 parentPort.postMessage({
515 id: ChargingStationWorkerMessageEvents.STARTED,
516 data: { id: this.stationInfo.chargingStationId },
517 });
518 }
519
520 public async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
521 // Stop message sequence
522 await this.stopMessageSequence(reason);
523 for (const connectorId of this.connectors.keys()) {
524 if (connectorId > 0) {
525 await this.ocppRequestService.sendMessageHandler(RequestCommand.STATUS_NOTIFICATION, {
526 connectorId,
527 status: ChargePointStatus.UNAVAILABLE,
528 errorCode: ChargePointErrorCode.NO_ERROR,
529 });
530 this.getConnectorStatus(connectorId).status = ChargePointStatus.UNAVAILABLE;
531 }
532 }
533 if (this.isWebSocketConnectionOpened()) {
534 this.wsConnection.close();
535 }
536 if (this.getEnableStatistics()) {
537 this.performanceStatistics.stop();
538 }
539 this.bootNotificationResponse = null;
540 parentPort.postMessage({
541 id: ChargingStationWorkerMessageEvents.STOPPED,
542 data: { id: this.stationInfo.chargingStationId },
543 });
544 this.stopped = true;
545 }
546
547 public getConfigurationKey(
548 key: string | StandardParametersKey,
549 caseInsensitive = false
550 ): ConfigurationKey | undefined {
551 return this.configuration.configurationKey.find((configElement) => {
552 if (caseInsensitive) {
553 return configElement.key.toLowerCase() === key.toLowerCase();
554 }
555 return configElement.key === key;
556 });
557 }
558
559 public addConfigurationKey(
560 key: string | StandardParametersKey,
561 value: string,
562 options: { readonly?: boolean; visible?: boolean; reboot?: boolean } = {
563 readonly: false,
564 visible: true,
565 reboot: false,
566 }
567 ): void {
568 const keyFound = this.getConfigurationKey(key);
569 const readonly = options.readonly;
570 const visible = options.visible;
571 const reboot = options.reboot;
572 if (!keyFound) {
573 this.configuration.configurationKey.push({
574 key,
575 readonly,
576 value,
577 visible,
578 reboot,
579 });
580 } else {
581 logger.error(
582 `${this.logPrefix()} Trying to add an already existing configuration key: %j`,
583 keyFound
584 );
585 }
586 }
587
588 public setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
589 const keyFound = this.getConfigurationKey(key);
590 if (keyFound) {
591 const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
592 this.configuration.configurationKey[keyIndex].value = value;
593 } else {
594 logger.error(
595 `${this.logPrefix()} Trying to set a value on a non existing configuration key: %j`,
596 { key, value }
597 );
598 }
599 }
600
601 public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
602 let cpReplaced = false;
603 if (!Utils.isEmptyArray(this.getConnectorStatus(connectorId).chargingProfiles)) {
604 this.getConnectorStatus(connectorId).chargingProfiles?.forEach(
605 (chargingProfile: ChargingProfile, index: number) => {
606 if (
607 chargingProfile.chargingProfileId === cp.chargingProfileId ||
608 (chargingProfile.stackLevel === cp.stackLevel &&
609 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
610 ) {
611 this.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
612 cpReplaced = true;
613 }
614 }
615 );
616 }
617 !cpReplaced && this.getConnectorStatus(connectorId).chargingProfiles?.push(cp);
618 }
619
620 public resetConnectorStatus(connectorId: number): void {
621 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
622 this.getConnectorStatus(connectorId).idTagAuthorized = false;
623 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
624 this.getConnectorStatus(connectorId).transactionStarted = false;
625 delete this.getConnectorStatus(connectorId).localAuthorizeIdTag;
626 delete this.getConnectorStatus(connectorId).authorizeIdTag;
627 delete this.getConnectorStatus(connectorId).transactionId;
628 delete this.getConnectorStatus(connectorId).transactionIdTag;
629 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
630 delete this.getConnectorStatus(connectorId).transactionBeginMeterValue;
631 this.stopMeterValues(connectorId);
632 }
633
634 public bufferMessage(message: string): void {
635 this.messageBuffer.add(message);
636 }
637
638 private flushMessageBuffer() {
639 if (this.messageBuffer.size > 0) {
640 this.messageBuffer.forEach((message) => {
641 // TODO: evaluate the need to track performance
642 this.wsConnection.send(message);
643 this.messageBuffer.delete(message);
644 });
645 }
646 }
647
648 private getSupervisionUrlOcppConfiguration(): boolean {
649 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
650 }
651
652 private getChargingStationId(stationTemplate: ChargingStationTemplate): string {
653 // In case of multiple instances: add instance index to charging station id
654 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
655 const idSuffix = stationTemplate.nameSuffix ?? '';
656 return stationTemplate.fixedName
657 ? stationTemplate.baseName
658 : stationTemplate.baseName +
659 '-' +
660 instanceIndex.toString() +
661 ('000000000' + this.index.toString()).substr(
662 ('000000000' + this.index.toString()).length - 4
663 ) +
664 idSuffix;
665 }
666
667 private buildStationInfo(): ChargingStationInfo {
668 let stationTemplateFromFile: ChargingStationTemplate;
669 try {
670 // Load template file
671 const fileDescriptor = fs.openSync(this.stationTemplateFile, 'r');
672 stationTemplateFromFile = JSON.parse(
673 fs.readFileSync(fileDescriptor, 'utf8')
674 ) as ChargingStationTemplate;
675 fs.closeSync(fileDescriptor);
676 } catch (error) {
677 FileUtils.handleFileException(
678 this.logPrefix(),
679 'Template',
680 this.stationTemplateFile,
681 error as NodeJS.ErrnoException
682 );
683 }
684 const chargingStationId = this.getChargingStationId(stationTemplateFromFile);
685 // Deprecation template keys section
686 this.warnDeprecatedTemplateKey(
687 stationTemplateFromFile,
688 'supervisionUrl',
689 chargingStationId,
690 "Use 'supervisionUrls' instead"
691 );
692 this.convertDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', 'supervisionUrls');
693 const stationInfo: ChargingStationInfo = stationTemplateFromFile ?? ({} as ChargingStationInfo);
694 stationInfo.wsOptions = stationTemplateFromFile?.wsOptions ?? {};
695 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
696 stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
697 const powerArrayRandomIndex = Math.floor(
698 Utils.secureRandom() * stationTemplateFromFile.power.length
699 );
700 stationInfo.maxPower =
701 stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
702 ? stationTemplateFromFile.power[powerArrayRandomIndex] * 1000
703 : stationTemplateFromFile.power[powerArrayRandomIndex];
704 } else {
705 stationTemplateFromFile.power = stationTemplateFromFile.power as number;
706 stationInfo.maxPower =
707 stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
708 ? stationTemplateFromFile.power * 1000
709 : stationTemplateFromFile.power;
710 }
711 delete stationInfo.power;
712 delete stationInfo.powerUnit;
713 stationInfo.chargingStationId = chargingStationId;
714 stationInfo.resetTime = stationTemplateFromFile.resetTime
715 ? stationTemplateFromFile.resetTime * 1000
716 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
717 return stationInfo;
718 }
719
720 private getOcppVersion(): OCPPVersion {
721 return this.stationInfo.ocppVersion ? this.stationInfo.ocppVersion : OCPPVersion.VERSION_16;
722 }
723
724 private handleUnsupportedVersion(version: OCPPVersion) {
725 const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${
726 this.stationTemplateFile
727 }`;
728 logger.error(errMsg);
729 throw new Error(errMsg);
730 }
731
732 private initialize(): void {
733 this.stationInfo = this.buildStationInfo();
734 this.configuration = this.getTemplateChargingStationConfiguration();
735 delete this.stationInfo.Configuration;
736 this.bootNotificationRequest = {
737 chargePointModel: this.stationInfo.chargePointModel,
738 chargePointVendor: this.stationInfo.chargePointVendor,
739 ...(!Utils.isUndefined(this.stationInfo.chargeBoxSerialNumberPrefix) && {
740 chargeBoxSerialNumber: this.stationInfo.chargeBoxSerialNumberPrefix,
741 }),
742 ...(!Utils.isUndefined(this.stationInfo.firmwareVersion) && {
743 firmwareVersion: this.stationInfo.firmwareVersion,
744 }),
745 };
746 // Build connectors if needed
747 const maxConnectors = this.getMaxNumberOfConnectors();
748 if (maxConnectors <= 0) {
749 logger.warn(
750 `${this.logPrefix()} Charging station template ${
751 this.stationTemplateFile
752 } with ${maxConnectors} connectors`
753 );
754 }
755 const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors();
756 if (templateMaxConnectors <= 0) {
757 logger.warn(
758 `${this.logPrefix()} Charging station template ${
759 this.stationTemplateFile
760 } with no connector configuration`
761 );
762 }
763 if (!this.stationInfo.Connectors[0]) {
764 logger.warn(
765 `${this.logPrefix()} Charging station template ${
766 this.stationTemplateFile
767 } with no connector Id 0 configuration`
768 );
769 }
770 // Sanity check
771 if (
772 maxConnectors >
773 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
774 !this.stationInfo.randomConnectors
775 ) {
776 logger.warn(
777 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
778 this.stationTemplateFile
779 }, forcing random connector configurations affectation`
780 );
781 this.stationInfo.randomConnectors = true;
782 }
783 const connectorsConfigHash = crypto
784 .createHash('sha256')
785 .update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString())
786 .digest('hex');
787 const connectorsConfigChanged =
788 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
789 if (this.connectors?.size === 0 || connectorsConfigChanged) {
790 connectorsConfigChanged && this.connectors.clear();
791 this.connectorsConfigurationHash = connectorsConfigHash;
792 // Add connector Id 0
793 let lastConnector = '0';
794 for (lastConnector in this.stationInfo.Connectors) {
795 const lastConnectorId = Utils.convertToInt(lastConnector);
796 if (
797 lastConnectorId === 0 &&
798 this.getUseConnectorId0() &&
799 this.stationInfo.Connectors[lastConnector]
800 ) {
801 this.connectors.set(
802 lastConnectorId,
803 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[lastConnector])
804 );
805 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
806 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
807 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
808 }
809 }
810 }
811 // Generate all connectors
812 if (
813 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0
814 ) {
815 for (let index = 1; index <= maxConnectors; index++) {
816 const randConnectorId = this.stationInfo.randomConnectors
817 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
818 : index;
819 this.connectors.set(
820 index,
821 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[randConnectorId])
822 );
823 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
824 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
825 this.getConnectorStatus(index).chargingProfiles = [];
826 }
827 }
828 }
829 }
830 // Avoid duplication of connectors related information
831 delete this.stationInfo.Connectors;
832 // Initialize transaction attributes on connectors
833 for (const connectorId of this.connectors.keys()) {
834 if (connectorId > 0 && !this.getConnectorStatus(connectorId)?.transactionStarted) {
835 this.initializeConnectorStatus(connectorId);
836 }
837 }
838 this.wsConfiguredConnectionUrl = new URL(
839 this.getConfiguredSupervisionUrl().href + '/' + this.stationInfo.chargingStationId
840 );
841 switch (this.getOcppVersion()) {
842 case OCPPVersion.VERSION_16:
843 this.ocppIncomingRequestService =
844 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>(this);
845 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
846 this,
847 OCPP16ResponseService.getInstance<OCPP16ResponseService>(this)
848 );
849 break;
850 default:
851 this.handleUnsupportedVersion(this.getOcppVersion());
852 break;
853 }
854 // OCPP parameters
855 this.initOcppParameters();
856 if (this.stationInfo.autoRegister) {
857 this.bootNotificationResponse = {
858 currentTime: new Date().toISOString(),
859 interval: this.getHeartbeatInterval() / 1000,
860 status: RegistrationStatus.ACCEPTED,
861 };
862 }
863 this.stationInfo.powerDivider = this.getPowerDivider();
864 if (this.getEnableStatistics()) {
865 this.performanceStatistics = PerformanceStatistics.getInstance(
866 this.id,
867 this.stationInfo.chargingStationId,
868 this.wsConnectionUrl
869 );
870 }
871 }
872
873 private initOcppParameters(): void {
874 if (
875 this.getSupervisionUrlOcppConfiguration() &&
876 !this.getConfigurationKey(
877 this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl
878 )
879 ) {
880 this.addConfigurationKey(
881 VendorDefaultParametersKey.ConnectionUrl,
882 this.getConfiguredSupervisionUrl().href,
883 { reboot: true }
884 );
885 }
886 if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) {
887 this.addConfigurationKey(
888 StandardParametersKey.SupportedFeatureProfiles,
889 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.Local_Auth_List_Management},${SupportedFeatureProfiles.Smart_Charging}`
890 );
891 }
892 this.addConfigurationKey(
893 StandardParametersKey.NumberOfConnectors,
894 this.getNumberOfConnectors().toString(),
895 { readonly: true }
896 );
897 if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
898 this.addConfigurationKey(
899 StandardParametersKey.MeterValuesSampledData,
900 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
901 );
902 }
903 if (!this.getConfigurationKey(StandardParametersKey.ConnectorPhaseRotation)) {
904 const connectorPhaseRotation = [];
905 for (const connectorId of this.connectors.keys()) {
906 // AC/DC
907 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
908 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
909 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
910 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
911 // AC
912 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
913 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
914 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
915 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
916 }
917 }
918 this.addConfigurationKey(
919 StandardParametersKey.ConnectorPhaseRotation,
920 connectorPhaseRotation.toString()
921 );
922 }
923 if (!this.getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests)) {
924 this.addConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
925 }
926 if (
927 !this.getConfigurationKey(StandardParametersKey.LocalAuthListEnabled) &&
928 this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles).value.includes(
929 SupportedFeatureProfiles.Local_Auth_List_Management
930 )
931 ) {
932 this.addConfigurationKey(StandardParametersKey.LocalAuthListEnabled, 'false');
933 }
934 if (!this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
935 this.addConfigurationKey(
936 StandardParametersKey.ConnectionTimeOut,
937 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
938 );
939 }
940 }
941
942 private async onOpen(): Promise<void> {
943 logger.info(
944 `${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`
945 );
946 if (!this.isInAcceptedState()) {
947 // Send BootNotification
948 let registrationRetryCount = 0;
949 do {
950 this.bootNotificationResponse = (await this.ocppRequestService.sendMessageHandler(
951 RequestCommand.BOOT_NOTIFICATION,
952 {
953 chargePointModel: this.bootNotificationRequest.chargePointModel,
954 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
955 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
956 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
957 },
958 { skipBufferingOnError: true }
959 )) as BootNotificationResponse;
960 if (!this.isInAcceptedState()) {
961 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
962 await Utils.sleep(
963 this.bootNotificationResponse?.interval
964 ? this.bootNotificationResponse.interval * 1000
965 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
966 );
967 }
968 } while (
969 !this.isInAcceptedState() &&
970 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
971 this.getRegistrationMaxRetries() === -1)
972 );
973 }
974 if (this.isInAcceptedState()) {
975 await this.startMessageSequence();
976 this.stopped && (this.stopped = false);
977 if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) {
978 this.flushMessageBuffer();
979 }
980 } else {
981 logger.error(
982 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
983 );
984 }
985 this.autoReconnectRetryCount = 0;
986 this.wsConnectionRestarted = false;
987 }
988
989 private async onClose(code: number, reason: string): Promise<void> {
990 switch (code) {
991 // Normal close
992 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
993 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
994 logger.info(
995 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
996 code
997 )}' and reason '${reason}'`
998 );
999 this.autoReconnectRetryCount = 0;
1000 break;
1001 // Abnormal close
1002 default:
1003 logger.error(
1004 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1005 code
1006 )}' and reason '${reason}'`
1007 );
1008 await this.reconnect(code);
1009 break;
1010 }
1011 }
1012
1013 private async onMessage(data: Data): Promise<void> {
1014 let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [
1015 0,
1016 '',
1017 '' as IncomingRequestCommand,
1018 {},
1019 {},
1020 ];
1021 let responseCallback: (
1022 payload: JsonType | string,
1023 requestPayload: JsonType | OCPPError
1024 ) => void;
1025 let rejectCallback: (error: OCPPError, requestStatistic?: boolean) => void;
1026 let requestCommandName: RequestCommand | IncomingRequestCommand;
1027 let requestPayload: JsonType | OCPPError;
1028 let cachedRequest: CachedRequest;
1029 let errMsg: string;
1030 try {
1031 const request = JSON.parse(data.toString()) as IncomingRequest;
1032 if (Utils.isIterable(request)) {
1033 // Parse the message
1034 [messageType, messageId, commandName, commandPayload, errorDetails] = request;
1035 } else {
1036 throw new OCPPError(
1037 ErrorType.PROTOCOL_ERROR,
1038 'Incoming request is not iterable',
1039 commandName
1040 );
1041 }
1042 // Check the Type of message
1043 switch (messageType) {
1044 // Incoming Message
1045 case MessageType.CALL_MESSAGE:
1046 if (this.getEnableStatistics()) {
1047 this.performanceStatistics.addRequestStatistic(commandName, messageType);
1048 }
1049 // Process the call
1050 await this.ocppIncomingRequestService.handleRequest(
1051 messageId,
1052 commandName,
1053 commandPayload
1054 );
1055 break;
1056 // Outcome Message
1057 case MessageType.CALL_RESULT_MESSAGE:
1058 // Respond
1059 cachedRequest = this.requests.get(messageId);
1060 if (Utils.isIterable(cachedRequest)) {
1061 [responseCallback, , , requestPayload] = cachedRequest;
1062 } else {
1063 throw new OCPPError(
1064 ErrorType.PROTOCOL_ERROR,
1065 `Cached request for message id ${messageId} response is not iterable`,
1066 commandName
1067 );
1068 }
1069 if (!responseCallback) {
1070 // Error
1071 throw new OCPPError(
1072 ErrorType.INTERNAL_ERROR,
1073 `Response for unknown message id ${messageId}`,
1074 commandName
1075 );
1076 }
1077 responseCallback(commandName, requestPayload);
1078 break;
1079 // Error Message
1080 case MessageType.CALL_ERROR_MESSAGE:
1081 cachedRequest = this.requests.get(messageId);
1082 if (Utils.isIterable(cachedRequest)) {
1083 [, rejectCallback, requestCommandName] = cachedRequest;
1084 } else {
1085 throw new OCPPError(
1086 ErrorType.PROTOCOL_ERROR,
1087 `Cached request for message id ${messageId} error response is not iterable`
1088 );
1089 }
1090 if (!rejectCallback) {
1091 // Error
1092 throw new OCPPError(
1093 ErrorType.INTERNAL_ERROR,
1094 `Error response for unknown message id ${messageId}`,
1095 requestCommandName
1096 );
1097 }
1098 rejectCallback(
1099 new OCPPError(commandName, commandPayload.toString(), requestCommandName, errorDetails)
1100 );
1101 break;
1102 // Error
1103 default:
1104 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1105 errMsg = `${this.logPrefix()} Wrong message type ${messageType}`;
1106 logger.error(errMsg);
1107 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1108 }
1109 } catch (error) {
1110 // Log
1111 logger.error(
1112 '%s Incoming OCPP message %j matching cached request %j processing error %j',
1113 this.logPrefix(),
1114 data.toString(),
1115 this.requests.get(messageId),
1116 error
1117 );
1118 // Send error
1119 messageType === MessageType.CALL_MESSAGE &&
1120 (await this.ocppRequestService.sendError(messageId, error as OCPPError, commandName));
1121 }
1122 }
1123
1124 private onPing(): void {
1125 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
1126 }
1127
1128 private onPong(): void {
1129 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
1130 }
1131
1132 private onError(error: WSError): void {
1133 logger.error(this.logPrefix() + ' WebSocket error: %j', error);
1134 }
1135
1136 private getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
1137 return this.stationInfo.Configuration ?? ({} as ChargingStationConfiguration);
1138 }
1139
1140 private getAuthorizationFile(): string | undefined {
1141 return (
1142 this.stationInfo.authorizationFile &&
1143 path.join(
1144 path.resolve(__dirname, '../'),
1145 'assets',
1146 path.basename(this.stationInfo.authorizationFile)
1147 )
1148 );
1149 }
1150
1151 private getAuthorizedTags(): string[] {
1152 let authorizedTags: string[] = [];
1153 const authorizationFile = this.getAuthorizationFile();
1154 if (authorizationFile) {
1155 try {
1156 // Load authorization file
1157 const fileDescriptor = fs.openSync(authorizationFile, 'r');
1158 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
1159 fs.closeSync(fileDescriptor);
1160 } catch (error) {
1161 FileUtils.handleFileException(
1162 this.logPrefix(),
1163 'Authorization',
1164 authorizationFile,
1165 error as NodeJS.ErrnoException
1166 );
1167 }
1168 } else {
1169 logger.info(
1170 this.logPrefix() +
1171 ' No authorization file given in template file ' +
1172 this.stationTemplateFile
1173 );
1174 }
1175 return authorizedTags;
1176 }
1177
1178 private getUseConnectorId0(): boolean | undefined {
1179 return !Utils.isUndefined(this.stationInfo.useConnectorId0)
1180 ? this.stationInfo.useConnectorId0
1181 : true;
1182 }
1183
1184 private getNumberOfRunningTransactions(): number {
1185 let trxCount = 0;
1186 for (const connectorId of this.connectors.keys()) {
1187 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1188 trxCount++;
1189 }
1190 }
1191 return trxCount;
1192 }
1193
1194 // 0 for disabling
1195 private getConnectionTimeout(): number | undefined {
1196 if (this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
1197 return (
1198 parseInt(this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut).value) ??
1199 Constants.DEFAULT_CONNECTION_TIMEOUT
1200 );
1201 }
1202 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1203 }
1204
1205 // -1 for unlimited, 0 for disabling
1206 private getAutoReconnectMaxRetries(): number | undefined {
1207 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1208 return this.stationInfo.autoReconnectMaxRetries;
1209 }
1210 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1211 return Configuration.getAutoReconnectMaxRetries();
1212 }
1213 return -1;
1214 }
1215
1216 // 0 for disabling
1217 private getRegistrationMaxRetries(): number | undefined {
1218 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1219 return this.stationInfo.registrationMaxRetries;
1220 }
1221 return -1;
1222 }
1223
1224 private getPowerDivider(): number {
1225 let powerDivider = this.getNumberOfConnectors();
1226 if (this.stationInfo.powerSharedByConnectors) {
1227 powerDivider = this.getNumberOfRunningTransactions();
1228 }
1229 return powerDivider;
1230 }
1231
1232 private getTemplateMaxNumberOfConnectors(): number {
1233 return Object.keys(this.stationInfo.Connectors).length;
1234 }
1235
1236 private getMaxNumberOfConnectors(): number {
1237 let maxConnectors: number;
1238 if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) {
1239 const numberOfConnectors = this.stationInfo.numberOfConnectors as number[];
1240 // Distribute evenly the number of connectors
1241 maxConnectors = numberOfConnectors[(this.index - 1) % numberOfConnectors.length];
1242 } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) {
1243 maxConnectors = this.stationInfo.numberOfConnectors as number;
1244 } else {
1245 maxConnectors = this.stationInfo.Connectors[0]
1246 ? this.getTemplateMaxNumberOfConnectors() - 1
1247 : this.getTemplateMaxNumberOfConnectors();
1248 }
1249 return maxConnectors;
1250 }
1251
1252 private async startMessageSequence(): Promise<void> {
1253 if (this.stationInfo.autoRegister) {
1254 await this.ocppRequestService.sendMessageHandler(
1255 RequestCommand.BOOT_NOTIFICATION,
1256 {
1257 chargePointModel: this.bootNotificationRequest.chargePointModel,
1258 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1259 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1260 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
1261 },
1262 { skipBufferingOnError: true }
1263 );
1264 }
1265 // Start WebSocket ping
1266 this.startWebSocketPing();
1267 // Start heartbeat
1268 this.startHeartbeat();
1269 // Initialize connectors status
1270 for (const connectorId of this.connectors.keys()) {
1271 if (connectorId === 0) {
1272 continue;
1273 } else if (
1274 !this.stopped &&
1275 !this.getConnectorStatus(connectorId)?.status &&
1276 this.getConnectorStatus(connectorId)?.bootStatus
1277 ) {
1278 // Send status in template at startup
1279 await this.ocppRequestService.sendMessageHandler(RequestCommand.STATUS_NOTIFICATION, {
1280 connectorId,
1281 status: this.getConnectorStatus(connectorId).bootStatus,
1282 errorCode: ChargePointErrorCode.NO_ERROR,
1283 });
1284 this.getConnectorStatus(connectorId).status =
1285 this.getConnectorStatus(connectorId).bootStatus;
1286 } else if (
1287 this.stopped &&
1288 this.getConnectorStatus(connectorId)?.status &&
1289 this.getConnectorStatus(connectorId)?.bootStatus
1290 ) {
1291 // Send status in template after reset
1292 await this.ocppRequestService.sendMessageHandler(RequestCommand.STATUS_NOTIFICATION, {
1293 connectorId,
1294 status: this.getConnectorStatus(connectorId).bootStatus,
1295 errorCode: ChargePointErrorCode.NO_ERROR,
1296 });
1297 this.getConnectorStatus(connectorId).status =
1298 this.getConnectorStatus(connectorId).bootStatus;
1299 } else if (!this.stopped && this.getConnectorStatus(connectorId)?.status) {
1300 // Send previous status at template reload
1301 await this.ocppRequestService.sendMessageHandler(RequestCommand.STATUS_NOTIFICATION, {
1302 connectorId,
1303 status: this.getConnectorStatus(connectorId).status,
1304 errorCode: ChargePointErrorCode.NO_ERROR,
1305 });
1306 } else {
1307 // Send default status
1308 await this.ocppRequestService.sendMessageHandler(RequestCommand.STATUS_NOTIFICATION, {
1309 connectorId,
1310 status: ChargePointStatus.AVAILABLE,
1311 errorCode: ChargePointErrorCode.NO_ERROR,
1312 });
1313 this.getConnectorStatus(connectorId).status = ChargePointStatus.AVAILABLE;
1314 }
1315 }
1316 // Start the ATG
1317 this.startAutomaticTransactionGenerator();
1318 }
1319
1320 private startAutomaticTransactionGenerator() {
1321 if (this.stationInfo.AutomaticTransactionGenerator.enable) {
1322 if (!this.automaticTransactionGenerator) {
1323 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
1324 }
1325 if (!this.automaticTransactionGenerator.started) {
1326 this.automaticTransactionGenerator.start();
1327 }
1328 }
1329 }
1330
1331 private async stopMessageSequence(
1332 reason: StopTransactionReason = StopTransactionReason.NONE
1333 ): Promise<void> {
1334 // Stop WebSocket ping
1335 this.stopWebSocketPing();
1336 // Stop heartbeat
1337 this.stopHeartbeat();
1338 // Stop the ATG
1339 if (
1340 this.stationInfo.AutomaticTransactionGenerator.enable &&
1341 this.automaticTransactionGenerator?.started
1342 ) {
1343 this.automaticTransactionGenerator.stop();
1344 } else {
1345 for (const connectorId of this.connectors.keys()) {
1346 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1347 const transactionId = this.getConnectorStatus(connectorId).transactionId;
1348 if (
1349 this.getBeginEndMeterValues() &&
1350 this.getOcppStrictCompliance() &&
1351 !this.getOutOfOrderEndMeterValues()
1352 ) {
1353 // FIXME: Implement OCPP version agnostic helpers
1354 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
1355 this,
1356 connectorId,
1357 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
1358 );
1359 await this.ocppRequestService.sendMessageHandler(RequestCommand.METER_VALUES, {
1360 connectorId,
1361 transactionId,
1362 meterValue: transactionEndMeterValue,
1363 });
1364 }
1365 await this.ocppRequestService.sendMessageHandler(RequestCommand.STOP_TRANSACTION, {
1366 transactionId,
1367 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId),
1368 idTag: this.getTransactionIdTag(transactionId),
1369 reason,
1370 });
1371 }
1372 }
1373 }
1374 }
1375
1376 private startWebSocketPing(): void {
1377 const webSocketPingInterval: number = this.getConfigurationKey(
1378 StandardParametersKey.WebSocketPingInterval
1379 )
1380 ? Utils.convertToInt(
1381 this.getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value
1382 )
1383 : 0;
1384 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1385 this.webSocketPingSetInterval = setInterval(() => {
1386 if (this.isWebSocketConnectionOpened()) {
1387 this.wsConnection.ping((): void => {
1388 /* This is intentional */
1389 });
1390 }
1391 }, webSocketPingInterval * 1000);
1392 logger.info(
1393 this.logPrefix() +
1394 ' WebSocket ping started every ' +
1395 Utils.formatDurationSeconds(webSocketPingInterval)
1396 );
1397 } else if (this.webSocketPingSetInterval) {
1398 logger.info(
1399 this.logPrefix() +
1400 ' WebSocket ping every ' +
1401 Utils.formatDurationSeconds(webSocketPingInterval) +
1402 ' already started'
1403 );
1404 } else {
1405 logger.error(
1406 `${this.logPrefix()} WebSocket ping interval set to ${
1407 webSocketPingInterval
1408 ? Utils.formatDurationSeconds(webSocketPingInterval)
1409 : webSocketPingInterval
1410 }, not starting the WebSocket ping`
1411 );
1412 }
1413 }
1414
1415 private stopWebSocketPing(): void {
1416 if (this.webSocketPingSetInterval) {
1417 clearInterval(this.webSocketPingSetInterval);
1418 }
1419 }
1420
1421 private warnDeprecatedTemplateKey(
1422 template: ChargingStationTemplate,
1423 key: string,
1424 chargingStationId: string,
1425 logMsgToAppend = ''
1426 ): void {
1427 if (!Utils.isUndefined(template[key])) {
1428 const logPrefixStr = ` ${chargingStationId} |`;
1429 logger.warn(
1430 `${Utils.logPrefix(logPrefixStr)} Deprecated template key '${key}' usage in file '${
1431 this.stationTemplateFile
1432 }'${logMsgToAppend && '. ' + logMsgToAppend}`
1433 );
1434 }
1435 }
1436
1437 private convertDeprecatedTemplateKey(
1438 template: ChargingStationTemplate,
1439 deprecatedKey: string,
1440 key: string
1441 ): void {
1442 if (!Utils.isUndefined(template[deprecatedKey])) {
1443 template[key] = template[deprecatedKey] as unknown;
1444 delete template[deprecatedKey];
1445 }
1446 }
1447
1448 private getConfiguredSupervisionUrl(): URL {
1449 const supervisionUrls = Utils.cloneObject<string | string[]>(
1450 this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
1451 );
1452 if (!Utils.isEmptyArray(supervisionUrls)) {
1453 let urlIndex = 0;
1454 switch (Configuration.getSupervisionUrlDistribution()) {
1455 case SupervisionUrlDistribution.ROUND_ROBIN:
1456 urlIndex = (this.index - 1) % supervisionUrls.length;
1457 break;
1458 case SupervisionUrlDistribution.RANDOM:
1459 // Get a random url
1460 urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1461 break;
1462 case SupervisionUrlDistribution.SEQUENTIAL:
1463 if (this.index <= supervisionUrls.length) {
1464 urlIndex = this.index - 1;
1465 } else {
1466 logger.warn(
1467 `${this.logPrefix()} No more configured supervision urls available, using the first one`
1468 );
1469 }
1470 break;
1471 default:
1472 logger.error(
1473 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1474 SupervisionUrlDistribution.ROUND_ROBIN
1475 }`
1476 );
1477 urlIndex = (this.index - 1) % supervisionUrls.length;
1478 break;
1479 }
1480 return new URL(supervisionUrls[urlIndex]);
1481 }
1482 return new URL(supervisionUrls as string);
1483 }
1484
1485 private getHeartbeatInterval(): number | undefined {
1486 const HeartbeatInterval = this.getConfigurationKey(StandardParametersKey.HeartbeatInterval);
1487 if (HeartbeatInterval) {
1488 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1489 }
1490 const HeartBeatInterval = this.getConfigurationKey(StandardParametersKey.HeartBeatInterval);
1491 if (HeartBeatInterval) {
1492 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
1493 }
1494 !this.stationInfo.autoRegister &&
1495 logger.warn(
1496 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1497 Constants.DEFAULT_HEARTBEAT_INTERVAL
1498 }`
1499 );
1500 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
1501 }
1502
1503 private stopHeartbeat(): void {
1504 if (this.heartbeatSetInterval) {
1505 clearInterval(this.heartbeatSetInterval);
1506 }
1507 }
1508
1509 private openWSConnection(
1510 options: ClientOptions & ClientRequestArgs = this.stationInfo.wsOptions,
1511 forceCloseOpened = false
1512 ): void {
1513 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
1514 if (
1515 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
1516 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
1517 ) {
1518 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
1519 }
1520 if (this.isWebSocketConnectionOpened() && forceCloseOpened) {
1521 this.wsConnection.close();
1522 }
1523 let protocol: string;
1524 switch (this.getOcppVersion()) {
1525 case OCPPVersion.VERSION_16:
1526 protocol = 'ocpp' + OCPPVersion.VERSION_16;
1527 break;
1528 default:
1529 this.handleUnsupportedVersion(this.getOcppVersion());
1530 break;
1531 }
1532 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
1533 logger.info(
1534 this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl.toString()
1535 );
1536 }
1537
1538 private stopMeterValues(connectorId: number) {
1539 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1540 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
1541 }
1542 }
1543
1544 private startAuthorizationFileMonitoring(): void {
1545 const authorizationFile = this.getAuthorizationFile();
1546 if (authorizationFile) {
1547 try {
1548 fs.watch(authorizationFile, (event, filename) => {
1549 if (filename && event === 'change') {
1550 try {
1551 logger.debug(
1552 this.logPrefix() +
1553 ' Authorization file ' +
1554 authorizationFile +
1555 ' have changed, reload'
1556 );
1557 // Initialize authorizedTags
1558 this.authorizedTags = this.getAuthorizedTags();
1559 } catch (error) {
1560 logger.error(this.logPrefix() + ' Authorization file monitoring error: %j', error);
1561 }
1562 }
1563 });
1564 } catch (error) {
1565 FileUtils.handleFileException(
1566 this.logPrefix(),
1567 'Authorization',
1568 authorizationFile,
1569 error as NodeJS.ErrnoException
1570 );
1571 }
1572 } else {
1573 logger.info(
1574 this.logPrefix() +
1575 ' No authorization file given in template file ' +
1576 this.stationTemplateFile +
1577 '. Not monitoring changes'
1578 );
1579 }
1580 }
1581
1582 private startStationTemplateFileMonitoring(): void {
1583 try {
1584 fs.watch(this.stationTemplateFile, (event, filename): void => {
1585 if (filename && event === 'change') {
1586 try {
1587 logger.debug(
1588 this.logPrefix() +
1589 ' Template file ' +
1590 this.stationTemplateFile +
1591 ' have changed, reload'
1592 );
1593 // Initialize
1594 this.initialize();
1595 // Restart the ATG
1596 if (
1597 !this.stationInfo.AutomaticTransactionGenerator.enable &&
1598 this.automaticTransactionGenerator
1599 ) {
1600 this.automaticTransactionGenerator.stop();
1601 }
1602 this.startAutomaticTransactionGenerator();
1603 if (this.getEnableStatistics()) {
1604 this.performanceStatistics.restart();
1605 } else {
1606 this.performanceStatistics.stop();
1607 }
1608 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
1609 } catch (error) {
1610 logger.error(
1611 this.logPrefix() + ' Charging station template file monitoring error: %j',
1612 error
1613 );
1614 }
1615 }
1616 });
1617 } catch (error) {
1618 FileUtils.handleFileException(
1619 this.logPrefix(),
1620 'Template',
1621 this.stationTemplateFile,
1622 error as NodeJS.ErrnoException
1623 );
1624 }
1625 }
1626
1627 private getReconnectExponentialDelay(): boolean | undefined {
1628 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
1629 ? this.stationInfo.reconnectExponentialDelay
1630 : false;
1631 }
1632
1633 private async reconnect(code: number): Promise<void> {
1634 // Stop WebSocket ping
1635 this.stopWebSocketPing();
1636 // Stop heartbeat
1637 this.stopHeartbeat();
1638 // Stop the ATG if needed
1639 if (
1640 this.stationInfo.AutomaticTransactionGenerator.enable &&
1641 this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
1642 this.automaticTransactionGenerator?.started
1643 ) {
1644 this.automaticTransactionGenerator.stop();
1645 }
1646 if (
1647 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
1648 this.getAutoReconnectMaxRetries() === -1
1649 ) {
1650 this.autoReconnectRetryCount++;
1651 const reconnectDelay = this.getReconnectExponentialDelay()
1652 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
1653 : this.getConnectionTimeout() * 1000;
1654 const reconnectTimeout = reconnectDelay - 100 > 0 && reconnectDelay;
1655 logger.error(
1656 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
1657 reconnectDelay,
1658 2
1659 )}ms, timeout ${reconnectTimeout}ms`
1660 );
1661 await Utils.sleep(reconnectDelay);
1662 logger.error(
1663 this.logPrefix() +
1664 ' WebSocket: reconnecting try #' +
1665 this.autoReconnectRetryCount.toString()
1666 );
1667 this.openWSConnection(
1668 { ...this.stationInfo.wsOptions, handshakeTimeout: reconnectTimeout },
1669 true
1670 );
1671 this.wsConnectionRestarted = true;
1672 } else if (this.getAutoReconnectMaxRetries() !== -1) {
1673 logger.error(
1674 `${this.logPrefix()} WebSocket reconnect failure: max retries reached (${
1675 this.autoReconnectRetryCount
1676 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
1677 );
1678 }
1679 }
1680
1681 private initializeConnectorStatus(connectorId: number): void {
1682 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
1683 this.getConnectorStatus(connectorId).idTagAuthorized = false;
1684 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
1685 this.getConnectorStatus(connectorId).transactionStarted = false;
1686 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
1687 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
1688 }
1689 }