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