7d633eb07b97940002922cbefae236dad8e3e39a
[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 | undefined {
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 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
656 }
657
658 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles) {
659 return ChargingStationConfigurationUtils.getConfigurationKey(
660 this,
661 StandardParametersKey.SupportedFeatureProfiles
662 )?.value.includes(featureProfile);
663 }
664
665 public bufferMessage(message: string): void {
666 this.messageBuffer.add(message);
667 }
668
669 public openWSConnection(
670 options: WsOptions = this.stationInfo?.wsOptions ?? {},
671 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
672 closeOpened: false,
673 terminateOpened: false,
674 }
675 ): void {
676 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
677 params.closeOpened = params?.closeOpened ?? false;
678 params.terminateOpened = params?.terminateOpened ?? false;
679 if (
680 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
681 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
682 ) {
683 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
684 }
685 if (params?.closeOpened) {
686 this.closeWSConnection();
687 }
688 if (params?.terminateOpened) {
689 this.terminateWSConnection();
690 }
691 let protocol: string;
692 switch (this.getOcppVersion()) {
693 case OCPPVersion.VERSION_16:
694 protocol = 'ocpp' + OCPPVersion.VERSION_16;
695 break;
696 default:
697 this.handleUnsupportedVersion(this.getOcppVersion());
698 break;
699 }
700
701 if (this.isWebSocketConnectionOpened()) {
702 logger.warn(
703 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
704 );
705 return;
706 }
707
708 logger.info(
709 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
710 );
711
712 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
713
714 // Handle WebSocket message
715 this.wsConnection.on(
716 'message',
717 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
718 );
719 // Handle WebSocket error
720 this.wsConnection.on(
721 'error',
722 this.onError.bind(this) as (this: WebSocket, error: Error) => void
723 );
724 // Handle WebSocket close
725 this.wsConnection.on(
726 'close',
727 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
728 );
729 // Handle WebSocket open
730 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
731 // Handle WebSocket ping
732 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
733 // Handle WebSocket pong
734 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
735 }
736
737 public closeWSConnection(): void {
738 if (this.isWebSocketConnectionOpened()) {
739 this.wsConnection.close();
740 this.wsConnection = null;
741 }
742 }
743
744 public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
745 if (!this.automaticTransactionGenerator) {
746 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
747 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
748 this
749 );
750 }
751 if (!Utils.isEmptyArray(connectorIds)) {
752 for (const connectorId of connectorIds) {
753 this.automaticTransactionGenerator.startConnector(connectorId);
754 }
755 } else {
756 this.automaticTransactionGenerator.start();
757 }
758 }
759
760 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
761 if (!Utils.isEmptyArray(connectorIds)) {
762 for (const connectorId of connectorIds) {
763 this.automaticTransactionGenerator?.stopConnector(connectorId);
764 }
765 } else {
766 this.automaticTransactionGenerator?.stop();
767 this.automaticTransactionGenerator = null;
768 }
769 }
770
771 public getNumberOfRunningTransactions(): number {
772 let trxCount = 0;
773 for (const connectorId of this.connectors.keys()) {
774 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
775 trxCount++;
776 }
777 }
778 return trxCount;
779 }
780
781 public async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
782 for (const connectorId of this.connectors.keys()) {
783 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
784 await this.stopTransactionOnConnector(connectorId, reason);
785 }
786 }
787 }
788
789 public async stopTransactionOnConnector(
790 connectorId: number,
791 reason = StopTransactionReason.NONE
792 ): Promise<StopTransactionResponse> {
793 const transactionId = this.getConnectorStatus(connectorId).transactionId;
794 if (
795 this.getBeginEndMeterValues() &&
796 this.getOcppStrictCompliance() &&
797 !this.getOutOfOrderEndMeterValues()
798 ) {
799 // FIXME: Implement OCPP version agnostic helpers
800 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
801 this,
802 connectorId,
803 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
804 );
805 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
806 this,
807 RequestCommand.METER_VALUES,
808 {
809 connectorId,
810 transactionId,
811 meterValue: [transactionEndMeterValue],
812 }
813 );
814 }
815 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
816 this,
817 RequestCommand.STOP_TRANSACTION,
818 {
819 transactionId,
820 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
821 idTag: this.getTransactionIdTag(transactionId),
822 reason,
823 }
824 );
825 }
826
827 private flushMessageBuffer(): void {
828 if (this.messageBuffer.size > 0) {
829 this.messageBuffer.forEach((message) => {
830 // TODO: evaluate the need to track performance
831 this.wsConnection.send(message);
832 this.messageBuffer.delete(message);
833 });
834 }
835 }
836
837 private getSupervisionUrlOcppConfiguration(): boolean {
838 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
839 }
840
841 private getSupervisionUrlOcppKey(): string {
842 return this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl;
843 }
844
845 private getTemplateFromFile(): ChargingStationTemplate | null {
846 let template: ChargingStationTemplate = null;
847 try {
848 if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) {
849 template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash);
850 } else {
851 const measureId = `${FileType.ChargingStationTemplate} read`;
852 const beginId = PerformanceStatistics.beginMeasure(measureId);
853 template = JSON.parse(
854 fs.readFileSync(this.templateFile, 'utf8')
855 ) as ChargingStationTemplate;
856 PerformanceStatistics.endMeasure(measureId, beginId);
857 template.templateHash = crypto
858 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
859 .update(JSON.stringify(template))
860 .digest('hex');
861 this.sharedLRUCache.setChargingStationTemplate(template);
862 }
863 } catch (error) {
864 FileUtils.handleFileException(
865 this.logPrefix(),
866 FileType.ChargingStationTemplate,
867 this.templateFile,
868 error as NodeJS.ErrnoException
869 );
870 }
871 return template;
872 }
873
874 private getStationInfoFromTemplate(): ChargingStationInfo {
875 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile();
876 if (Utils.isNullOrUndefined(stationTemplate)) {
877 const errorMsg = 'Failed to read charging station template file';
878 logger.error(`${this.logPrefix()} ${errorMsg}`);
879 throw new BaseError(errorMsg);
880 }
881 if (Utils.isEmptyObject(stationTemplate)) {
882 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
883 logger.error(`${this.logPrefix()} ${errorMsg}`);
884 throw new BaseError(errorMsg);
885 }
886 // Deprecation template keys section
887 ChargingStationUtils.warnDeprecatedTemplateKey(
888 stationTemplate,
889 'supervisionUrl',
890 this.templateFile,
891 this.logPrefix(),
892 "Use 'supervisionUrls' instead"
893 );
894 ChargingStationUtils.convertDeprecatedTemplateKey(
895 stationTemplate,
896 'supervisionUrl',
897 'supervisionUrls'
898 );
899 const stationInfo: ChargingStationInfo =
900 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
901 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
902 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
903 this.index,
904 stationTemplate
905 );
906 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
907 if (!Utils.isEmptyArray(stationTemplate.power)) {
908 stationTemplate.power = stationTemplate.power as number[];
909 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
910 stationInfo.maximumPower =
911 stationTemplate.powerUnit === PowerUnits.KILO_WATT
912 ? stationTemplate.power[powerArrayRandomIndex] * 1000
913 : stationTemplate.power[powerArrayRandomIndex];
914 } else {
915 stationTemplate.power = stationTemplate.power as number;
916 stationInfo.maximumPower =
917 stationTemplate.powerUnit === PowerUnits.KILO_WATT
918 ? stationTemplate.power * 1000
919 : stationTemplate.power;
920 }
921 stationInfo.resetTime = stationTemplate.resetTime
922 ? stationTemplate.resetTime * 1000
923 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
924 const configuredMaxConnectors = ChargingStationUtils.getConfiguredNumberOfConnectors(
925 this.index,
926 stationTemplate
927 );
928 ChargingStationUtils.checkConfiguredMaxConnectors(
929 configuredMaxConnectors,
930 this.templateFile,
931 this.logPrefix()
932 );
933 const templateMaxConnectors =
934 ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
935 ChargingStationUtils.checkTemplateMaxConnectors(
936 templateMaxConnectors,
937 this.templateFile,
938 this.logPrefix()
939 );
940 if (
941 configuredMaxConnectors >
942 (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
943 !stationTemplate?.randomConnectors
944 ) {
945 logger.warn(
946 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
947 this.templateFile
948 }, forcing random connector configurations affectation`
949 );
950 stationInfo.randomConnectors = true;
951 }
952 // Build connectors if needed (FIXME: should be factored out)
953 this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors);
954 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
955 ChargingStationUtils.createStationInfoHash(stationInfo);
956 return stationInfo;
957 }
958
959 private getStationInfoFromFile(): ChargingStationInfo | null {
960 let stationInfo: ChargingStationInfo = null;
961 this.getStationInfoPersistentConfiguration() &&
962 (stationInfo = this.getConfigurationFromFile()?.stationInfo ?? null);
963 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
964 return stationInfo;
965 }
966
967 private getStationInfo(): ChargingStationInfo {
968 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
969 const stationInfoFromFile: ChargingStationInfo = this.getStationInfoFromFile();
970 // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
971 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
972 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
973 return this.stationInfo;
974 }
975 return stationInfoFromFile;
976 }
977 stationInfoFromFile &&
978 ChargingStationUtils.propagateSerialNumber(
979 this.getTemplateFromFile(),
980 stationInfoFromFile,
981 stationInfoFromTemplate
982 );
983 return stationInfoFromTemplate;
984 }
985
986 private saveStationInfo(): void {
987 if (this.getStationInfoPersistentConfiguration()) {
988 this.saveConfiguration();
989 }
990 }
991
992 private getOcppVersion(): OCPPVersion {
993 return this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
994 }
995
996 private getOcppPersistentConfiguration(): boolean {
997 return this.stationInfo?.ocppPersistentConfiguration ?? true;
998 }
999
1000 private getStationInfoPersistentConfiguration(): boolean {
1001 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
1002 }
1003
1004 private handleUnsupportedVersion(version: OCPPVersion) {
1005 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
1006 logger.error(`${this.logPrefix()} ${errMsg}`);
1007 throw new BaseError(errMsg);
1008 }
1009
1010 private initialize(): void {
1011 this.configurationFile = path.join(
1012 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
1013 ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile()) + '.json'
1014 );
1015 this.stationInfo = this.getStationInfo();
1016 this.saveStationInfo();
1017 logger.info(`${this.logPrefix()} Charging station hashId '${this.stationInfo.hashId}'`);
1018 // Avoid duplication of connectors related information in RAM
1019 this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
1020 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
1021 if (this.getEnableStatistics()) {
1022 this.performanceStatistics = PerformanceStatistics.getInstance(
1023 this.stationInfo.hashId,
1024 this.stationInfo.chargingStationId,
1025 this.configuredSupervisionUrl
1026 );
1027 }
1028 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1029 this.stationInfo
1030 );
1031 this.powerDivider = this.getPowerDivider();
1032 // OCPP configuration
1033 this.ocppConfiguration = this.getOcppConfiguration();
1034 this.initializeOcppConfiguration();
1035 switch (this.getOcppVersion()) {
1036 case OCPPVersion.VERSION_16:
1037 this.ocppIncomingRequestService =
1038 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
1039 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1040 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1041 );
1042 break;
1043 default:
1044 this.handleUnsupportedVersion(this.getOcppVersion());
1045 break;
1046 }
1047 if (this.stationInfo?.autoRegister) {
1048 this.bootNotificationResponse = {
1049 currentTime: new Date().toISOString(),
1050 interval: this.getHeartbeatInterval() / 1000,
1051 status: RegistrationStatus.ACCEPTED,
1052 };
1053 }
1054 }
1055
1056 private initializeOcppConfiguration(): void {
1057 if (
1058 !ChargingStationConfigurationUtils.getConfigurationKey(
1059 this,
1060 StandardParametersKey.HeartbeatInterval
1061 )
1062 ) {
1063 ChargingStationConfigurationUtils.addConfigurationKey(
1064 this,
1065 StandardParametersKey.HeartbeatInterval,
1066 '0'
1067 );
1068 }
1069 if (
1070 !ChargingStationConfigurationUtils.getConfigurationKey(
1071 this,
1072 StandardParametersKey.HeartBeatInterval
1073 )
1074 ) {
1075 ChargingStationConfigurationUtils.addConfigurationKey(
1076 this,
1077 StandardParametersKey.HeartBeatInterval,
1078 '0',
1079 { visible: false }
1080 );
1081 }
1082 if (
1083 this.getSupervisionUrlOcppConfiguration() &&
1084 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1085 ) {
1086 ChargingStationConfigurationUtils.addConfigurationKey(
1087 this,
1088 this.getSupervisionUrlOcppKey(),
1089 this.configuredSupervisionUrl.href,
1090 { reboot: true }
1091 );
1092 } else if (
1093 !this.getSupervisionUrlOcppConfiguration() &&
1094 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1095 ) {
1096 ChargingStationConfigurationUtils.deleteConfigurationKey(
1097 this,
1098 this.getSupervisionUrlOcppKey(),
1099 { save: false }
1100 );
1101 }
1102 if (
1103 this.stationInfo.amperageLimitationOcppKey &&
1104 !ChargingStationConfigurationUtils.getConfigurationKey(
1105 this,
1106 this.stationInfo.amperageLimitationOcppKey
1107 )
1108 ) {
1109 ChargingStationConfigurationUtils.addConfigurationKey(
1110 this,
1111 this.stationInfo.amperageLimitationOcppKey,
1112 (
1113 this.stationInfo.maximumAmperage *
1114 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1115 ).toString()
1116 );
1117 }
1118 if (
1119 !ChargingStationConfigurationUtils.getConfigurationKey(
1120 this,
1121 StandardParametersKey.SupportedFeatureProfiles
1122 )
1123 ) {
1124 ChargingStationConfigurationUtils.addConfigurationKey(
1125 this,
1126 StandardParametersKey.SupportedFeatureProfiles,
1127 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1128 );
1129 }
1130 ChargingStationConfigurationUtils.addConfigurationKey(
1131 this,
1132 StandardParametersKey.NumberOfConnectors,
1133 this.getNumberOfConnectors().toString(),
1134 { readonly: true },
1135 { overwrite: true }
1136 );
1137 if (
1138 !ChargingStationConfigurationUtils.getConfigurationKey(
1139 this,
1140 StandardParametersKey.MeterValuesSampledData
1141 )
1142 ) {
1143 ChargingStationConfigurationUtils.addConfigurationKey(
1144 this,
1145 StandardParametersKey.MeterValuesSampledData,
1146 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1147 );
1148 }
1149 if (
1150 !ChargingStationConfigurationUtils.getConfigurationKey(
1151 this,
1152 StandardParametersKey.ConnectorPhaseRotation
1153 )
1154 ) {
1155 const connectorPhaseRotation = [];
1156 for (const connectorId of this.connectors.keys()) {
1157 // AC/DC
1158 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1159 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1160 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1161 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1162 // AC
1163 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1164 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1165 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1166 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1167 }
1168 }
1169 ChargingStationConfigurationUtils.addConfigurationKey(
1170 this,
1171 StandardParametersKey.ConnectorPhaseRotation,
1172 connectorPhaseRotation.toString()
1173 );
1174 }
1175 if (
1176 !ChargingStationConfigurationUtils.getConfigurationKey(
1177 this,
1178 StandardParametersKey.AuthorizeRemoteTxRequests
1179 )
1180 ) {
1181 ChargingStationConfigurationUtils.addConfigurationKey(
1182 this,
1183 StandardParametersKey.AuthorizeRemoteTxRequests,
1184 'true'
1185 );
1186 }
1187 if (
1188 !ChargingStationConfigurationUtils.getConfigurationKey(
1189 this,
1190 StandardParametersKey.LocalAuthListEnabled
1191 ) &&
1192 ChargingStationConfigurationUtils.getConfigurationKey(
1193 this,
1194 StandardParametersKey.SupportedFeatureProfiles
1195 )?.value.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1196 ) {
1197 ChargingStationConfigurationUtils.addConfigurationKey(
1198 this,
1199 StandardParametersKey.LocalAuthListEnabled,
1200 'false'
1201 );
1202 }
1203 if (
1204 !ChargingStationConfigurationUtils.getConfigurationKey(
1205 this,
1206 StandardParametersKey.ConnectionTimeOut
1207 )
1208 ) {
1209 ChargingStationConfigurationUtils.addConfigurationKey(
1210 this,
1211 StandardParametersKey.ConnectionTimeOut,
1212 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1213 );
1214 }
1215 this.saveOcppConfiguration();
1216 }
1217
1218 private initializeConnectors(
1219 stationInfo: ChargingStationInfo,
1220 configuredMaxConnectors: number,
1221 templateMaxConnectors: number
1222 ): void {
1223 if (!stationInfo?.Connectors && this.connectors.size === 0) {
1224 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1225 logger.error(`${this.logPrefix()} ${logMsg}`);
1226 throw new BaseError(logMsg);
1227 }
1228 if (!stationInfo?.Connectors[0]) {
1229 logger.warn(
1230 `${this.logPrefix()} Charging station information from template ${
1231 this.templateFile
1232 } with no connector Id 0 configuration`
1233 );
1234 }
1235 if (stationInfo?.Connectors) {
1236 const connectorsConfigHash = crypto
1237 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1238 .update(JSON.stringify(stationInfo?.Connectors) + configuredMaxConnectors.toString())
1239 .digest('hex');
1240 const connectorsConfigChanged =
1241 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1242 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1243 connectorsConfigChanged && this.connectors.clear();
1244 this.connectorsConfigurationHash = connectorsConfigHash;
1245 // Add connector Id 0
1246 let lastConnector = '0';
1247 for (lastConnector in stationInfo?.Connectors) {
1248 const lastConnectorId = Utils.convertToInt(lastConnector);
1249 if (
1250 lastConnectorId === 0 &&
1251 this.getUseConnectorId0(stationInfo) &&
1252 stationInfo?.Connectors[lastConnector]
1253 ) {
1254 this.connectors.set(
1255 lastConnectorId,
1256 Utils.cloneObject<ConnectorStatus>(stationInfo?.Connectors[lastConnector])
1257 );
1258 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1259 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1260 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1261 }
1262 }
1263 }
1264 // Generate all connectors
1265 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
1266 for (let index = 1; index <= configuredMaxConnectors; index++) {
1267 const randConnectorId = stationInfo?.randomConnectors
1268 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1269 : index;
1270 this.connectors.set(
1271 index,
1272 Utils.cloneObject<ConnectorStatus>(stationInfo?.Connectors[randConnectorId])
1273 );
1274 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1275 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1276 this.getConnectorStatus(index).chargingProfiles = [];
1277 }
1278 }
1279 }
1280 }
1281 } else {
1282 logger.warn(
1283 `${this.logPrefix()} Charging station information from template ${
1284 this.templateFile
1285 } with no connectors configuration defined, using already defined connectors`
1286 );
1287 }
1288 // Initialize transaction attributes on connectors
1289 for (const connectorId of this.connectors.keys()) {
1290 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === false) {
1291 this.initializeConnectorStatus(connectorId);
1292 }
1293 }
1294 }
1295
1296 private getConfigurationFromFile(): ChargingStationConfiguration | null {
1297 let configuration: ChargingStationConfiguration = null;
1298 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1299 try {
1300 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1301 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1302 this.configurationFileHash
1303 );
1304 } else {
1305 const measureId = `${FileType.ChargingStationConfiguration} read`;
1306 const beginId = PerformanceStatistics.beginMeasure(measureId);
1307 configuration = JSON.parse(
1308 fs.readFileSync(this.configurationFile, 'utf8')
1309 ) as ChargingStationConfiguration;
1310 PerformanceStatistics.endMeasure(measureId, beginId);
1311 this.configurationFileHash = configuration.configurationHash;
1312 this.sharedLRUCache.setChargingStationConfiguration(configuration);
1313 }
1314 } catch (error) {
1315 FileUtils.handleFileException(
1316 this.logPrefix(),
1317 FileType.ChargingStationConfiguration,
1318 this.configurationFile,
1319 error as NodeJS.ErrnoException
1320 );
1321 }
1322 }
1323 return configuration;
1324 }
1325
1326 private saveConfiguration(): void {
1327 if (this.configurationFile) {
1328 try {
1329 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1330 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1331 }
1332 const configurationData: ChargingStationConfiguration =
1333 this.getConfigurationFromFile() ?? {};
1334 this.ocppConfiguration?.configurationKey &&
1335 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1336 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1337 delete configurationData.configurationHash;
1338 const configurationHash = crypto
1339 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1340 .update(JSON.stringify(configurationData))
1341 .digest('hex');
1342 if (this.configurationFileHash !== configurationHash) {
1343 configurationData.configurationHash = configurationHash;
1344 const measureId = `${FileType.ChargingStationConfiguration} write`;
1345 const beginId = PerformanceStatistics.beginMeasure(measureId);
1346 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1347 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1348 fs.closeSync(fileDescriptor);
1349 PerformanceStatistics.endMeasure(measureId, beginId);
1350 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
1351 this.configurationFileHash = configurationHash;
1352 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
1353 } else {
1354 logger.debug(
1355 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1356 this.configurationFile
1357 }`
1358 );
1359 }
1360 } catch (error) {
1361 FileUtils.handleFileException(
1362 this.logPrefix(),
1363 FileType.ChargingStationConfiguration,
1364 this.configurationFile,
1365 error as NodeJS.ErrnoException
1366 );
1367 }
1368 } else {
1369 logger.error(
1370 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1371 );
1372 }
1373 }
1374
1375 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | null {
1376 return this.getTemplateFromFile()?.Configuration ?? null;
1377 }
1378
1379 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | null {
1380 let configuration: ChargingStationConfiguration = null;
1381 if (this.getOcppPersistentConfiguration()) {
1382 const configurationFromFile = this.getConfigurationFromFile();
1383 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1384 }
1385 configuration && delete configuration.stationInfo;
1386 return configuration;
1387 }
1388
1389 private getOcppConfiguration(): ChargingStationOcppConfiguration | null {
1390 let ocppConfiguration: ChargingStationOcppConfiguration = this.getOcppConfigurationFromFile();
1391 if (!ocppConfiguration) {
1392 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1393 }
1394 return ocppConfiguration;
1395 }
1396
1397 private async onOpen(): Promise<void> {
1398 if (this.isWebSocketConnectionOpened()) {
1399 logger.info(
1400 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1401 );
1402 if (!this.isRegistered()) {
1403 // Send BootNotification
1404 let registrationRetryCount = 0;
1405 do {
1406 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1407 BootNotificationRequest,
1408 BootNotificationResponse
1409 >(
1410 this,
1411 RequestCommand.BOOT_NOTIFICATION,
1412 {
1413 chargePointModel: this.bootNotificationRequest.chargePointModel,
1414 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1415 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1416 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
1417 chargePointSerialNumber: this.bootNotificationRequest.chargePointSerialNumber,
1418 iccid: this.bootNotificationRequest.iccid,
1419 imsi: this.bootNotificationRequest.imsi,
1420 meterSerialNumber: this.bootNotificationRequest.meterSerialNumber,
1421 meterType: this.bootNotificationRequest.meterType,
1422 },
1423 { skipBufferingOnError: true }
1424 );
1425 if (!this.isRegistered()) {
1426 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1427 await Utils.sleep(
1428 this.bootNotificationResponse?.interval
1429 ? this.bootNotificationResponse.interval * 1000
1430 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1431 );
1432 }
1433 } while (
1434 !this.isRegistered() &&
1435 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1436 this.getRegistrationMaxRetries() === -1)
1437 );
1438 }
1439 if (this.isRegistered()) {
1440 if (this.isInAcceptedState()) {
1441 await this.startMessageSequence();
1442 this.wsConnectionRestarted && this.flushMessageBuffer();
1443 }
1444 } else {
1445 logger.error(
1446 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1447 );
1448 }
1449 this.started === false && (this.started = true);
1450 this.autoReconnectRetryCount = 0;
1451 this.wsConnectionRestarted = false;
1452 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1453 } else {
1454 logger.warn(
1455 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1456 );
1457 }
1458 }
1459
1460 private async onClose(code: number, reason: string): Promise<void> {
1461 switch (code) {
1462 // Normal close
1463 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1464 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1465 logger.info(
1466 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1467 code
1468 )}' and reason '${reason}'`
1469 );
1470 this.autoReconnectRetryCount = 0;
1471 await this.stopMessageSequence(StopTransactionReason.OTHER);
1472 break;
1473 // Abnormal close
1474 default:
1475 logger.error(
1476 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1477 code
1478 )}' and reason '${reason}'`
1479 );
1480 await this.reconnect(code);
1481 break;
1482 }
1483 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1484 }
1485
1486 private async onMessage(data: Data): Promise<void> {
1487 let messageType: number;
1488 let messageId: string;
1489 let commandName: IncomingRequestCommand;
1490 let commandPayload: JsonType;
1491 let errorType: ErrorType;
1492 let errorMessage: string;
1493 let errorDetails: JsonType;
1494 let responseCallback: (payload: JsonType, requestPayload: JsonType) => void;
1495 let errorCallback: (error: OCPPError, requestStatistic?: boolean) => void;
1496 let requestCommandName: RequestCommand | IncomingRequestCommand;
1497 let requestPayload: JsonType;
1498 let cachedRequest: CachedRequest;
1499 let errMsg: string;
1500 try {
1501 const request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1502 if (Array.isArray(request) === true) {
1503 [messageType, messageId] = request;
1504 // Check the type of message
1505 switch (messageType) {
1506 // Incoming Message
1507 case MessageType.CALL_MESSAGE:
1508 [, , commandName, commandPayload] = request as IncomingRequest;
1509 if (this.getEnableStatistics()) {
1510 this.performanceStatistics.addRequestStatistic(commandName, messageType);
1511 }
1512 logger.debug(
1513 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1514 request
1515 )}`
1516 );
1517 // Process the message
1518 await this.ocppIncomingRequestService.incomingRequestHandler(
1519 this,
1520 messageId,
1521 commandName,
1522 commandPayload
1523 );
1524 break;
1525 // Outcome Message
1526 case MessageType.CALL_RESULT_MESSAGE:
1527 [, , commandPayload] = request as Response;
1528 if (!this.requests.has(messageId)) {
1529 // Error
1530 throw new OCPPError(
1531 ErrorType.INTERNAL_ERROR,
1532 `Response for unknown message id ${messageId}`,
1533 null,
1534 commandPayload
1535 );
1536 }
1537 // Respond
1538 cachedRequest = this.requests.get(messageId);
1539 if (Array.isArray(cachedRequest) === true) {
1540 [responseCallback, , requestCommandName, requestPayload] = cachedRequest;
1541 } else {
1542 throw new OCPPError(
1543 ErrorType.PROTOCOL_ERROR,
1544 `Cached request for message id ${messageId} response is not an array`,
1545 null,
1546 cachedRequest as unknown as JsonType
1547 );
1548 }
1549 logger.debug(
1550 `${this.logPrefix()} << Command '${
1551 requestCommandName ?? 'unknown'
1552 }' received response payload: ${JSON.stringify(request)}`
1553 );
1554 responseCallback(commandPayload, requestPayload);
1555 break;
1556 // Error Message
1557 case MessageType.CALL_ERROR_MESSAGE:
1558 [, , errorType, errorMessage, errorDetails] = request as ErrorResponse;
1559 if (!this.requests.has(messageId)) {
1560 // Error
1561 throw new OCPPError(
1562 ErrorType.INTERNAL_ERROR,
1563 `Error response for unknown message id ${messageId}`,
1564 null,
1565 { errorType, errorMessage, errorDetails }
1566 );
1567 }
1568 cachedRequest = this.requests.get(messageId);
1569 if (Array.isArray(cachedRequest) === true) {
1570 [, errorCallback, requestCommandName] = cachedRequest;
1571 } else {
1572 throw new OCPPError(
1573 ErrorType.PROTOCOL_ERROR,
1574 `Cached request for message id ${messageId} error response is not an array`,
1575 null,
1576 cachedRequest as unknown as JsonType
1577 );
1578 }
1579 logger.debug(
1580 `${this.logPrefix()} << Command '${
1581 requestCommandName ?? 'unknown'
1582 }' received error payload: ${JSON.stringify(request)}`
1583 );
1584 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1585 break;
1586 // Error
1587 default:
1588 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1589 errMsg = `Wrong message type ${messageType}`;
1590 logger.error(`${this.logPrefix()} ${errMsg}`);
1591 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1592 }
1593 parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1594 } else {
1595 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1596 payload: request,
1597 });
1598 }
1599 } catch (error) {
1600 // Log
1601 logger.error(
1602 `${this.logPrefix()} Incoming OCPP command '${
1603 commandName ?? requestCommandName ?? null
1604 }' message '${data.toString()}' matching cached request '${JSON.stringify(
1605 this.requests.get(messageId)
1606 )}' processing error:`,
1607 error
1608 );
1609 if (!(error instanceof OCPPError)) {
1610 logger.warn(
1611 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1612 commandName ?? requestCommandName ?? null
1613 }' message '${data.toString()}' handling is not an OCPPError:`,
1614 error
1615 );
1616 }
1617 // Send error
1618 messageType === MessageType.CALL_MESSAGE &&
1619 (await this.ocppRequestService.sendError(
1620 this,
1621 messageId,
1622 error as OCPPError,
1623 commandName ?? requestCommandName ?? null
1624 ));
1625 }
1626 }
1627
1628 private onPing(): void {
1629 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
1630 }
1631
1632 private onPong(): void {
1633 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
1634 }
1635
1636 private onError(error: WSError): void {
1637 this.closeWSConnection();
1638 logger.error(this.logPrefix() + ' WebSocket error:', error);
1639 }
1640
1641 private getEnergyActiveImportRegister(
1642 connectorStatus: ConnectorStatus,
1643 meterStop = false
1644 ): number {
1645 if (this.getMeteringPerTransaction()) {
1646 return (
1647 (meterStop === true
1648 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1649 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1650 );
1651 }
1652 return (
1653 (meterStop === true
1654 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1655 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1656 );
1657 }
1658
1659 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean | undefined {
1660 const localStationInfo = stationInfo ?? this.stationInfo;
1661 return !Utils.isUndefined(localStationInfo.useConnectorId0)
1662 ? localStationInfo.useConnectorId0
1663 : true;
1664 }
1665
1666 // 0 for disabling
1667 private getConnectionTimeout(): number | undefined {
1668 if (
1669 ChargingStationConfigurationUtils.getConfigurationKey(
1670 this,
1671 StandardParametersKey.ConnectionTimeOut
1672 )
1673 ) {
1674 return (
1675 parseInt(
1676 ChargingStationConfigurationUtils.getConfigurationKey(
1677 this,
1678 StandardParametersKey.ConnectionTimeOut
1679 ).value
1680 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1681 );
1682 }
1683 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1684 }
1685
1686 // -1 for unlimited, 0 for disabling
1687 private getAutoReconnectMaxRetries(): number | undefined {
1688 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1689 return this.stationInfo.autoReconnectMaxRetries;
1690 }
1691 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1692 return Configuration.getAutoReconnectMaxRetries();
1693 }
1694 return -1;
1695 }
1696
1697 // 0 for disabling
1698 private getRegistrationMaxRetries(): number | undefined {
1699 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1700 return this.stationInfo.registrationMaxRetries;
1701 }
1702 return -1;
1703 }
1704
1705 private getPowerDivider(): number {
1706 let powerDivider = this.getNumberOfConnectors();
1707 if (this.stationInfo?.powerSharedByConnectors) {
1708 powerDivider = this.getNumberOfRunningTransactions();
1709 }
1710 return powerDivider;
1711 }
1712
1713 private getMaximumPower(stationInfo?: ChargingStationInfo): number {
1714 const localStationInfo = stationInfo ?? this.stationInfo;
1715 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
1716 }
1717
1718 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1719 const maximumPower = this.getMaximumPower(stationInfo);
1720 switch (this.getCurrentOutType(stationInfo)) {
1721 case CurrentType.AC:
1722 return ACElectricUtils.amperagePerPhaseFromPower(
1723 this.getNumberOfPhases(stationInfo),
1724 maximumPower / this.getNumberOfConnectors(),
1725 this.getVoltageOut(stationInfo)
1726 );
1727 case CurrentType.DC:
1728 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1729 }
1730 }
1731
1732 private getAmperageLimitation(): number | undefined {
1733 if (
1734 this.stationInfo.amperageLimitationOcppKey &&
1735 ChargingStationConfigurationUtils.getConfigurationKey(
1736 this,
1737 this.stationInfo.amperageLimitationOcppKey
1738 )
1739 ) {
1740 return (
1741 Utils.convertToInt(
1742 ChargingStationConfigurationUtils.getConfigurationKey(
1743 this,
1744 this.stationInfo.amperageLimitationOcppKey
1745 ).value
1746 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1747 );
1748 }
1749 }
1750
1751 private async startMessageSequence(): Promise<void> {
1752 if (this.stationInfo?.autoRegister) {
1753 await this.ocppRequestService.requestHandler<
1754 BootNotificationRequest,
1755 BootNotificationResponse
1756 >(
1757 this,
1758 RequestCommand.BOOT_NOTIFICATION,
1759 {
1760 chargePointModel: this.bootNotificationRequest.chargePointModel,
1761 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1762 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1763 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
1764 chargePointSerialNumber: this.bootNotificationRequest.chargePointSerialNumber,
1765 iccid: this.bootNotificationRequest.iccid,
1766 imsi: this.bootNotificationRequest.imsi,
1767 meterSerialNumber: this.bootNotificationRequest.meterSerialNumber,
1768 meterType: this.bootNotificationRequest.meterType,
1769 },
1770 { skipBufferingOnError: true }
1771 );
1772 }
1773 // Start WebSocket ping
1774 this.startWebSocketPing();
1775 // Start heartbeat
1776 this.startHeartbeat();
1777 // Initialize connectors status
1778 for (const connectorId of this.connectors.keys()) {
1779 if (connectorId === 0) {
1780 continue;
1781 } else if (
1782 this.started === true &&
1783 !this.getConnectorStatus(connectorId)?.status &&
1784 this.getConnectorStatus(connectorId)?.bootStatus
1785 ) {
1786 // Send status in template at startup
1787 await this.ocppRequestService.requestHandler<
1788 StatusNotificationRequest,
1789 StatusNotificationResponse
1790 >(this, RequestCommand.STATUS_NOTIFICATION, {
1791 connectorId,
1792 status: this.getConnectorStatus(connectorId).bootStatus,
1793 errorCode: ChargePointErrorCode.NO_ERROR,
1794 });
1795 this.getConnectorStatus(connectorId).status =
1796 this.getConnectorStatus(connectorId).bootStatus;
1797 } else if (
1798 this.started === false &&
1799 this.getConnectorStatus(connectorId)?.status &&
1800 this.getConnectorStatus(connectorId)?.bootStatus
1801 ) {
1802 // Send status in template after reset
1803 await this.ocppRequestService.requestHandler<
1804 StatusNotificationRequest,
1805 StatusNotificationResponse
1806 >(this, RequestCommand.STATUS_NOTIFICATION, {
1807 connectorId,
1808 status: this.getConnectorStatus(connectorId).bootStatus,
1809 errorCode: ChargePointErrorCode.NO_ERROR,
1810 });
1811 this.getConnectorStatus(connectorId).status =
1812 this.getConnectorStatus(connectorId).bootStatus;
1813 } else if (this.started === true && this.getConnectorStatus(connectorId)?.status) {
1814 // Send previous status at template reload
1815 await this.ocppRequestService.requestHandler<
1816 StatusNotificationRequest,
1817 StatusNotificationResponse
1818 >(this, RequestCommand.STATUS_NOTIFICATION, {
1819 connectorId,
1820 status: this.getConnectorStatus(connectorId).status,
1821 errorCode: ChargePointErrorCode.NO_ERROR,
1822 });
1823 } else {
1824 // Send default status
1825 await this.ocppRequestService.requestHandler<
1826 StatusNotificationRequest,
1827 StatusNotificationResponse
1828 >(this, RequestCommand.STATUS_NOTIFICATION, {
1829 connectorId,
1830 status: ChargePointStatus.AVAILABLE,
1831 errorCode: ChargePointErrorCode.NO_ERROR,
1832 });
1833 this.getConnectorStatus(connectorId).status = ChargePointStatus.AVAILABLE;
1834 }
1835 }
1836 // Start the ATG
1837 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable) {
1838 this.startAutomaticTransactionGenerator();
1839 }
1840 }
1841
1842 private async stopMessageSequence(
1843 reason: StopTransactionReason = StopTransactionReason.NONE
1844 ): Promise<void> {
1845 // Stop WebSocket ping
1846 this.stopWebSocketPing();
1847 // Stop heartbeat
1848 this.stopHeartbeat();
1849 // Stop ongoing transactions
1850 if (this.getNumberOfRunningTransactions() > 0) {
1851 if (this.automaticTransactionGenerator?.started) {
1852 this.stopAutomaticTransactionGenerator();
1853 } else {
1854 await this.stopRunningTransactions(reason);
1855 }
1856 }
1857 }
1858
1859 private startWebSocketPing(): void {
1860 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1861 this,
1862 StandardParametersKey.WebSocketPingInterval
1863 )
1864 ? Utils.convertToInt(
1865 ChargingStationConfigurationUtils.getConfigurationKey(
1866 this,
1867 StandardParametersKey.WebSocketPingInterval
1868 ).value
1869 )
1870 : 0;
1871 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1872 this.webSocketPingSetInterval = setInterval(() => {
1873 if (this.isWebSocketConnectionOpened()) {
1874 this.wsConnection.ping((): void => {
1875 /* This is intentional */
1876 });
1877 }
1878 }, webSocketPingInterval * 1000);
1879 logger.info(
1880 this.logPrefix() +
1881 ' WebSocket ping started every ' +
1882 Utils.formatDurationSeconds(webSocketPingInterval)
1883 );
1884 } else if (this.webSocketPingSetInterval) {
1885 logger.info(
1886 this.logPrefix() +
1887 ' WebSocket ping every ' +
1888 Utils.formatDurationSeconds(webSocketPingInterval) +
1889 ' already started'
1890 );
1891 } else {
1892 logger.error(
1893 `${this.logPrefix()} WebSocket ping interval set to ${
1894 webSocketPingInterval
1895 ? Utils.formatDurationSeconds(webSocketPingInterval)
1896 : webSocketPingInterval
1897 }, not starting the WebSocket ping`
1898 );
1899 }
1900 }
1901
1902 private stopWebSocketPing(): void {
1903 if (this.webSocketPingSetInterval) {
1904 clearInterval(this.webSocketPingSetInterval);
1905 }
1906 }
1907
1908 private getConfiguredSupervisionUrl(): URL {
1909 const supervisionUrls = Utils.cloneObject<string | string[]>(
1910 this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
1911 );
1912 if (!Utils.isEmptyArray(supervisionUrls)) {
1913 let urlIndex = 0;
1914 switch (Configuration.getSupervisionUrlDistribution()) {
1915 case SupervisionUrlDistribution.ROUND_ROBIN:
1916 urlIndex = (this.index - 1) % supervisionUrls.length;
1917 break;
1918 case SupervisionUrlDistribution.RANDOM:
1919 // Get a random url
1920 urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1921 break;
1922 case SupervisionUrlDistribution.SEQUENTIAL:
1923 if (this.index <= supervisionUrls.length) {
1924 urlIndex = this.index - 1;
1925 } else {
1926 logger.warn(
1927 `${this.logPrefix()} No more configured supervision urls available, using the first one`
1928 );
1929 }
1930 break;
1931 default:
1932 logger.error(
1933 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1934 SupervisionUrlDistribution.ROUND_ROBIN
1935 }`
1936 );
1937 urlIndex = (this.index - 1) % supervisionUrls.length;
1938 break;
1939 }
1940 return new URL(supervisionUrls[urlIndex]);
1941 }
1942 return new URL(supervisionUrls as string);
1943 }
1944
1945 private getHeartbeatInterval(): number | undefined {
1946 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1947 this,
1948 StandardParametersKey.HeartbeatInterval
1949 );
1950 if (HeartbeatInterval) {
1951 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1952 }
1953 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1954 this,
1955 StandardParametersKey.HeartBeatInterval
1956 );
1957 if (HeartBeatInterval) {
1958 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
1959 }
1960 !this.stationInfo?.autoRegister &&
1961 logger.warn(
1962 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1963 Constants.DEFAULT_HEARTBEAT_INTERVAL
1964 }`
1965 );
1966 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
1967 }
1968
1969 private stopHeartbeat(): void {
1970 if (this.heartbeatSetInterval) {
1971 clearInterval(this.heartbeatSetInterval);
1972 }
1973 }
1974
1975 private terminateWSConnection(): void {
1976 if (this.isWebSocketConnectionOpened()) {
1977 this.wsConnection.terminate();
1978 this.wsConnection = null;
1979 }
1980 }
1981
1982 private stopMeterValues(connectorId: number) {
1983 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1984 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
1985 }
1986 }
1987
1988 private getReconnectExponentialDelay(): boolean | undefined {
1989 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
1990 ? this.stationInfo.reconnectExponentialDelay
1991 : false;
1992 }
1993
1994 private async reconnect(code: number): Promise<void> {
1995 // Stop WebSocket ping
1996 this.stopWebSocketPing();
1997 // Stop heartbeat
1998 this.stopHeartbeat();
1999 // Stop the ATG if needed
2000 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2001 this.stopAutomaticTransactionGenerator();
2002 }
2003 if (
2004 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2005 this.getAutoReconnectMaxRetries() === -1
2006 ) {
2007 this.autoReconnectRetryCount++;
2008 const reconnectDelay = this.getReconnectExponentialDelay()
2009 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2010 : this.getConnectionTimeout() * 1000;
2011 const reconnectDelayWithdraw = 1000;
2012 const reconnectTimeout =
2013 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2014 ? reconnectDelay - reconnectDelayWithdraw
2015 : 0;
2016 logger.error(
2017 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
2018 reconnectDelay,
2019 2
2020 )}ms, timeout ${reconnectTimeout}ms`
2021 );
2022 await Utils.sleep(reconnectDelay);
2023 logger.error(
2024 this.logPrefix() +
2025 ' WebSocket: reconnecting try #' +
2026 this.autoReconnectRetryCount.toString()
2027 );
2028 this.openWSConnection(
2029 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
2030 { closeOpened: true }
2031 );
2032 this.wsConnectionRestarted = true;
2033 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2034 await this.stopMessageSequence(StopTransactionReason.OTHER);
2035 logger.error(
2036 `${this.logPrefix()} WebSocket reconnect failure: maximum retries reached (${
2037 this.autoReconnectRetryCount
2038 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
2039 );
2040 }
2041 }
2042
2043 private getAutomaticTransactionGeneratorConfigurationFromTemplate(): AutomaticTransactionGeneratorConfiguration | null {
2044 return this.getTemplateFromFile()?.AutomaticTransactionGenerator ?? null;
2045 }
2046
2047 private initializeConnectorStatus(connectorId: number): void {
2048 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2049 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2050 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
2051 this.getConnectorStatus(connectorId).transactionStarted = false;
2052 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2053 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
2054 }
2055 }