Strict null check fixes
[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 | null;
105 public wsConnection!: WebSocket | null;
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 | undefined;
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 | undefined {
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 | undefined {
803 let template: ChargingStationTemplate;
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 | undefined = 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 = 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 | null = 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 = 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 = 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 | null =
1413 this.getOcppConfigurationFromFile();
1414 if (!ocppConfiguration) {
1415 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1416 }
1417 return ocppConfiguration;
1418 }
1419
1420 private async onOpen(): Promise<void> {
1421 if (this.isWebSocketConnectionOpened() === true) {
1422 logger.info(
1423 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1424 );
1425 if (this.isRegistered() === false) {
1426 // Send BootNotification
1427 let registrationRetryCount = 0;
1428 do {
1429 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1430 BootNotificationRequest,
1431 BootNotificationResponse
1432 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1433 skipBufferingOnError: true,
1434 });
1435 if (this.isRegistered() === false) {
1436 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1437 await Utils.sleep(
1438 this?.bootNotificationResponse?.interval
1439 ? this.bootNotificationResponse.interval * 1000
1440 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1441 );
1442 }
1443 } while (
1444 this.isRegistered() === false &&
1445 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1446 this.getRegistrationMaxRetries() === -1)
1447 );
1448 }
1449 if (this.isRegistered() === true) {
1450 if (this.isInAcceptedState() === true) {
1451 await this.startMessageSequence();
1452 }
1453 } else {
1454 logger.error(
1455 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1456 );
1457 }
1458 this.wsConnectionRestarted = false;
1459 this.autoReconnectRetryCount = 0;
1460 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1461 } else {
1462 logger.warn(
1463 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1464 );
1465 }
1466 }
1467
1468 private async onClose(code: number, reason: Buffer): Promise<void> {
1469 switch (code) {
1470 // Normal close
1471 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1472 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1473 logger.info(
1474 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1475 code
1476 )}' and reason '${reason.toString()}'`
1477 );
1478 this.autoReconnectRetryCount = 0;
1479 break;
1480 // Abnormal close
1481 default:
1482 logger.error(
1483 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1484 code
1485 )}' and reason '${reason.toString()}'`
1486 );
1487 this.started === true && (await this.reconnect());
1488 break;
1489 }
1490 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1491 }
1492
1493 private async onMessage(data: RawData): Promise<void> {
1494 let messageType: number;
1495 let messageId: string;
1496 let commandName: IncomingRequestCommand;
1497 let commandPayload: JsonType;
1498 let errorType: ErrorType;
1499 let errorMessage: string;
1500 let errorDetails: JsonType;
1501 let responseCallback: ResponseCallback;
1502 let errorCallback: ErrorCallback;
1503 let requestCommandName: RequestCommand | IncomingRequestCommand;
1504 let requestPayload: JsonType;
1505 let cachedRequest: CachedRequest;
1506 let errMsg: string;
1507 try {
1508 const request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1509 if (Array.isArray(request) === true) {
1510 [messageType, messageId] = request;
1511 // Check the type of message
1512 switch (messageType) {
1513 // Incoming Message
1514 case MessageType.CALL_MESSAGE:
1515 [, , commandName, commandPayload] = request as IncomingRequest;
1516 if (this.getEnableStatistics() === true) {
1517 this.performanceStatistics.addRequestStatistic(commandName, messageType);
1518 }
1519 logger.debug(
1520 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1521 request
1522 )}`
1523 );
1524 // Process the message
1525 await this.ocppIncomingRequestService.incomingRequestHandler(
1526 this,
1527 messageId,
1528 commandName,
1529 commandPayload
1530 );
1531 break;
1532 // Outcome Message
1533 case MessageType.CALL_RESULT_MESSAGE:
1534 [, , commandPayload] = request as Response;
1535 if (this.requests.has(messageId) === false) {
1536 // Error
1537 throw new OCPPError(
1538 ErrorType.INTERNAL_ERROR,
1539 `Response for unknown message id ${messageId}`,
1540 undefined,
1541 commandPayload
1542 );
1543 }
1544 // Respond
1545 cachedRequest = this.requests.get(messageId);
1546 if (Array.isArray(cachedRequest) === true) {
1547 [responseCallback, errorCallback, requestCommandName, requestPayload] = cachedRequest;
1548 } else {
1549 throw new OCPPError(
1550 ErrorType.PROTOCOL_ERROR,
1551 `Cached request for message id ${messageId} response is not an array`,
1552 undefined,
1553 cachedRequest as unknown as JsonType
1554 );
1555 }
1556 logger.debug(
1557 `${this.logPrefix()} << Command '${
1558 requestCommandName ?? Constants.UNKNOWN_COMMAND
1559 }' received response payload: ${JSON.stringify(request)}`
1560 );
1561 responseCallback(commandPayload, requestPayload);
1562 break;
1563 // Error Message
1564 case MessageType.CALL_ERROR_MESSAGE:
1565 [, , errorType, errorMessage, errorDetails] = request as ErrorResponse;
1566 if (this.requests.has(messageId) === false) {
1567 // Error
1568 throw new OCPPError(
1569 ErrorType.INTERNAL_ERROR,
1570 `Error response for unknown message id ${messageId}`,
1571 undefined,
1572 { errorType, errorMessage, errorDetails }
1573 );
1574 }
1575 cachedRequest = this.requests.get(messageId);
1576 if (Array.isArray(cachedRequest) === true) {
1577 [, errorCallback, requestCommandName] = cachedRequest;
1578 } else {
1579 throw new OCPPError(
1580 ErrorType.PROTOCOL_ERROR,
1581 `Cached request for message id ${messageId} error response is not an array`,
1582 undefined,
1583 cachedRequest as unknown as JsonType
1584 );
1585 }
1586 logger.debug(
1587 `${this.logPrefix()} << Command '${
1588 requestCommandName ?? Constants.UNKNOWN_COMMAND
1589 }' received error response payload: ${JSON.stringify(request)}`
1590 );
1591 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1592 break;
1593 // Error
1594 default:
1595 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1596 errMsg = `Wrong message type ${messageType}`;
1597 logger.error(`${this.logPrefix()} ${errMsg}`);
1598 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1599 }
1600 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
1601 } else {
1602 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
1603 request,
1604 });
1605 }
1606 } catch (error) {
1607 // Log
1608 logger.error(
1609 `${this.logPrefix()} Incoming OCPP command '${
1610 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1611 }' message '${data.toString()}'${
1612 messageType !== MessageType.CALL_MESSAGE
1613 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1614 : ''
1615 } processing error:`,
1616 error
1617 );
1618 if (error instanceof OCPPError === false) {
1619 logger.warn(
1620 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1621 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1622 }' message '${data.toString()}' handling is not an OCPPError:`,
1623 error
1624 );
1625 }
1626 switch (messageType) {
1627 case MessageType.CALL_MESSAGE:
1628 // Send error
1629 await this.ocppRequestService.sendError(
1630 this,
1631 messageId,
1632 error as OCPPError,
1633 commandName ?? requestCommandName ?? null
1634 );
1635 break;
1636 case MessageType.CALL_RESULT_MESSAGE:
1637 case MessageType.CALL_ERROR_MESSAGE:
1638 if (errorCallback) {
1639 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1640 errorCallback(error as OCPPError, false);
1641 } else {
1642 // Remove the request from the cache in case of error at response handling
1643 this.requests.delete(messageId);
1644 }
1645 break;
1646 }
1647 }
1648 }
1649
1650 private onPing(): void {
1651 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
1652 }
1653
1654 private onPong(): void {
1655 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
1656 }
1657
1658 private onError(error: WSError): void {
1659 this.closeWSConnection();
1660 logger.error(`${this.logPrefix()} WebSocket error:`, error);
1661 }
1662
1663 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
1664 if (this.getMeteringPerTransaction() === true) {
1665 return (
1666 (rounded === true
1667 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1668 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1669 );
1670 }
1671 return (
1672 (rounded === true
1673 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1674 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1675 );
1676 }
1677
1678 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1679 const localStationInfo = stationInfo ?? this.stationInfo;
1680 return !Utils.isUndefined(localStationInfo.useConnectorId0)
1681 ? localStationInfo.useConnectorId0
1682 : true;
1683 }
1684
1685 private getNumberOfRunningTransactions(): number {
1686 let trxCount = 0;
1687 for (const connectorId of this.connectors.keys()) {
1688 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1689 trxCount++;
1690 }
1691 }
1692 return trxCount;
1693 }
1694
1695 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1696 for (const connectorId of this.connectors.keys()) {
1697 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1698 await this.stopTransactionOnConnector(connectorId, reason);
1699 }
1700 }
1701 }
1702
1703 // 0 for disabling
1704 private getConnectionTimeout(): number {
1705 if (
1706 ChargingStationConfigurationUtils.getConfigurationKey(
1707 this,
1708 StandardParametersKey.ConnectionTimeOut
1709 )
1710 ) {
1711 return (
1712 parseInt(
1713 ChargingStationConfigurationUtils.getConfigurationKey(
1714 this,
1715 StandardParametersKey.ConnectionTimeOut
1716 ).value
1717 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1718 );
1719 }
1720 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1721 }
1722
1723 // -1 for unlimited, 0 for disabling
1724 private getAutoReconnectMaxRetries(): number | undefined {
1725 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1726 return this.stationInfo.autoReconnectMaxRetries;
1727 }
1728 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1729 return Configuration.getAutoReconnectMaxRetries();
1730 }
1731 return -1;
1732 }
1733
1734 // 0 for disabling
1735 private getRegistrationMaxRetries(): number | undefined {
1736 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1737 return this.stationInfo.registrationMaxRetries;
1738 }
1739 return -1;
1740 }
1741
1742 private getPowerDivider(): number {
1743 let powerDivider = this.getNumberOfConnectors();
1744 if (this.stationInfo?.powerSharedByConnectors) {
1745 powerDivider = this.getNumberOfRunningTransactions();
1746 }
1747 return powerDivider;
1748 }
1749
1750 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1751 const maximumPower = this.getMaximumPower(stationInfo);
1752 switch (this.getCurrentOutType(stationInfo)) {
1753 case CurrentType.AC:
1754 return ACElectricUtils.amperagePerPhaseFromPower(
1755 this.getNumberOfPhases(stationInfo),
1756 maximumPower / this.getNumberOfConnectors(),
1757 this.getVoltageOut(stationInfo)
1758 );
1759 case CurrentType.DC:
1760 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1761 }
1762 }
1763
1764 private getAmperageLimitation(): number | undefined {
1765 if (
1766 this.stationInfo.amperageLimitationOcppKey &&
1767 ChargingStationConfigurationUtils.getConfigurationKey(
1768 this,
1769 this.stationInfo.amperageLimitationOcppKey
1770 )
1771 ) {
1772 return (
1773 Utils.convertToInt(
1774 ChargingStationConfigurationUtils.getConfigurationKey(
1775 this,
1776 this.stationInfo.amperageLimitationOcppKey
1777 )?.value
1778 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1779 );
1780 }
1781 }
1782
1783 private async startMessageSequence(): Promise<void> {
1784 if (this.stationInfo?.autoRegister === true) {
1785 await this.ocppRequestService.requestHandler<
1786 BootNotificationRequest,
1787 BootNotificationResponse
1788 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1789 skipBufferingOnError: true,
1790 });
1791 }
1792 // Start WebSocket ping
1793 this.startWebSocketPing();
1794 // Start heartbeat
1795 this.startHeartbeat();
1796 // Initialize connectors status
1797 for (const connectorId of this.connectors.keys()) {
1798 let connectorStatus: ConnectorStatusEnum | undefined;
1799 if (connectorId === 0) {
1800 continue;
1801 } else if (
1802 !this.getConnectorStatus(connectorId)?.status &&
1803 (this.isChargingStationAvailable() === false ||
1804 this.isConnectorAvailable(connectorId) === false)
1805 ) {
1806 connectorStatus = ConnectorStatusEnum.UNAVAILABLE;
1807 } else if (
1808 !this.getConnectorStatus(connectorId)?.status &&
1809 this.getConnectorStatus(connectorId)?.bootStatus
1810 ) {
1811 // Set boot status in template at startup
1812 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
1813 } else if (this.getConnectorStatus(connectorId)?.status) {
1814 // Set previous status at startup
1815 connectorStatus = this.getConnectorStatus(connectorId)?.status;
1816 } else {
1817 // Set default status
1818 connectorStatus = ConnectorStatusEnum.AVAILABLE;
1819 }
1820 await this.ocppRequestService.requestHandler<
1821 StatusNotificationRequest,
1822 StatusNotificationResponse
1823 >(
1824 this,
1825 RequestCommand.STATUS_NOTIFICATION,
1826 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1827 );
1828 this.getConnectorStatus(connectorId).status = connectorStatus;
1829 }
1830 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1831 await this.ocppRequestService.requestHandler<
1832 FirmwareStatusNotificationRequest,
1833 FirmwareStatusNotificationResponse
1834 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1835 status: FirmwareStatus.Installed,
1836 });
1837 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
1838 }
1839
1840 // Start the ATG
1841 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
1842 this.startAutomaticTransactionGenerator();
1843 }
1844 this.wsConnectionRestarted === true && this.flushMessageBuffer();
1845 }
1846
1847 private async stopMessageSequence(
1848 reason: StopTransactionReason = StopTransactionReason.NONE
1849 ): Promise<void> {
1850 // Stop WebSocket ping
1851 this.stopWebSocketPing();
1852 // Stop heartbeat
1853 this.stopHeartbeat();
1854 // Stop ongoing transactions
1855 if (this.automaticTransactionGenerator?.started === true) {
1856 this.stopAutomaticTransactionGenerator();
1857 } else {
1858 await this.stopRunningTransactions(reason);
1859 }
1860 for (const connectorId of this.connectors.keys()) {
1861 if (connectorId > 0) {
1862 await this.ocppRequestService.requestHandler<
1863 StatusNotificationRequest,
1864 StatusNotificationResponse
1865 >(
1866 this,
1867 RequestCommand.STATUS_NOTIFICATION,
1868 OCPPServiceUtils.buildStatusNotificationRequest(
1869 this,
1870 connectorId,
1871 ConnectorStatusEnum.UNAVAILABLE
1872 )
1873 );
1874 this.getConnectorStatus(connectorId).status = undefined;
1875 }
1876 }
1877 }
1878
1879 private startWebSocketPing(): void {
1880 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1881 this,
1882 StandardParametersKey.WebSocketPingInterval
1883 )
1884 ? Utils.convertToInt(
1885 ChargingStationConfigurationUtils.getConfigurationKey(
1886 this,
1887 StandardParametersKey.WebSocketPingInterval
1888 )?.value
1889 )
1890 : 0;
1891 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1892 this.webSocketPingSetInterval = setInterval(() => {
1893 if (this.isWebSocketConnectionOpened() === true) {
1894 this.wsConnection?.ping();
1895 }
1896 }, webSocketPingInterval * 1000);
1897 logger.info(
1898 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
1899 webSocketPingInterval
1900 )}`
1901 );
1902 } else if (this.webSocketPingSetInterval) {
1903 logger.info(
1904 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
1905 webSocketPingInterval
1906 )}`
1907 );
1908 } else {
1909 logger.error(
1910 `${this.logPrefix()} WebSocket ping interval set to ${
1911 webSocketPingInterval
1912 ? Utils.formatDurationSeconds(webSocketPingInterval)
1913 : webSocketPingInterval
1914 }, not starting the WebSocket ping`
1915 );
1916 }
1917 }
1918
1919 private stopWebSocketPing(): void {
1920 if (this.webSocketPingSetInterval) {
1921 clearInterval(this.webSocketPingSetInterval);
1922 }
1923 }
1924
1925 private getConfiguredSupervisionUrl(): URL {
1926 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
1927 if (!Utils.isEmptyArray(supervisionUrls)) {
1928 switch (Configuration.getSupervisionUrlDistribution()) {
1929 case SupervisionUrlDistribution.ROUND_ROBIN:
1930 // FIXME
1931 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1932 break;
1933 case SupervisionUrlDistribution.RANDOM:
1934 this.configuredSupervisionUrlIndex = Math.floor(
1935 Utils.secureRandom() * supervisionUrls.length
1936 );
1937 break;
1938 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1939 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1940 break;
1941 default:
1942 logger.error(
1943 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1944 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
1945 }`
1946 );
1947 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1948 break;
1949 }
1950 return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
1951 }
1952 return new URL(supervisionUrls as string);
1953 }
1954
1955 private getHeartbeatInterval(): number {
1956 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1957 this,
1958 StandardParametersKey.HeartbeatInterval
1959 );
1960 if (HeartbeatInterval) {
1961 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1962 }
1963 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1964 this,
1965 StandardParametersKey.HeartBeatInterval
1966 );
1967 if (HeartBeatInterval) {
1968 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
1969 }
1970 this.stationInfo?.autoRegister === false &&
1971 logger.warn(
1972 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1973 Constants.DEFAULT_HEARTBEAT_INTERVAL
1974 }`
1975 );
1976 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
1977 }
1978
1979 private stopHeartbeat(): void {
1980 if (this.heartbeatSetInterval) {
1981 clearInterval(this.heartbeatSetInterval);
1982 }
1983 }
1984
1985 private terminateWSConnection(): void {
1986 if (this.isWebSocketConnectionOpened() === true) {
1987 this.wsConnection?.terminate();
1988 this.wsConnection = null;
1989 }
1990 }
1991
1992 private stopMeterValues(connectorId: number) {
1993 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1994 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
1995 }
1996 }
1997
1998 private getReconnectExponentialDelay(): boolean {
1999 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
2000 ? this.stationInfo.reconnectExponentialDelay
2001 : false;
2002 }
2003
2004 private async reconnect(): Promise<void> {
2005 // Stop WebSocket ping
2006 this.stopWebSocketPing();
2007 // Stop heartbeat
2008 this.stopHeartbeat();
2009 // Stop the ATG if needed
2010 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2011 this.stopAutomaticTransactionGenerator();
2012 }
2013 if (
2014 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2015 this.getAutoReconnectMaxRetries() === -1
2016 ) {
2017 this.autoReconnectRetryCount++;
2018 const reconnectDelay = this.getReconnectExponentialDelay()
2019 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2020 : this.getConnectionTimeout() * 1000;
2021 const reconnectDelayWithdraw = 1000;
2022 const reconnectTimeout =
2023 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2024 ? reconnectDelay - reconnectDelayWithdraw
2025 : 0;
2026 logger.error(
2027 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2028 reconnectDelay,
2029 2
2030 )}ms, timeout ${reconnectTimeout}ms`
2031 );
2032 await Utils.sleep(reconnectDelay);
2033 logger.error(
2034 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2035 );
2036 this.openWSConnection(
2037 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
2038 { closeOpened: true }
2039 );
2040 this.wsConnectionRestarted = true;
2041 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2042 logger.error(
2043 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2044 this.autoReconnectRetryCount
2045 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2046 );
2047 }
2048 }
2049
2050 private getAutomaticTransactionGeneratorConfigurationFromTemplate(): AutomaticTransactionGeneratorConfiguration | null {
2051 return this.getTemplateFromFile()?.AutomaticTransactionGenerator ?? null;
2052 }
2053
2054 private initializeConnectorStatus(connectorId: number): void {
2055 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2056 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2057 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
2058 this.getConnectorStatus(connectorId).transactionStarted = false;
2059 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2060 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
2061 }
2062 }