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