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