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