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