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