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