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