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