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