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