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