Ensure start transaction payload is always compliant with OCA specs
[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 rounded = false
345 ): number {
346 return this.getEnergyActiveImportRegister(
347 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
348 rounded
349 );
350 }
351
352 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
353 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded);
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(connectorStatus: ConnectorStatus, rounded = false): number {
1641 if (this.getMeteringPerTransaction() === true) {
1642 return (
1643 (rounded === true
1644 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1645 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1646 );
1647 }
1648 return (
1649 (rounded === true
1650 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1651 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1652 );
1653 }
1654
1655 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
1656 const localStationInfo = stationInfo ?? this.stationInfo;
1657 return !Utils.isUndefined(localStationInfo.useConnectorId0)
1658 ? localStationInfo.useConnectorId0
1659 : true;
1660 }
1661
1662 private getNumberOfRunningTransactions(): number {
1663 let trxCount = 0;
1664 for (const connectorId of this.connectors.keys()) {
1665 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1666 trxCount++;
1667 }
1668 }
1669 return trxCount;
1670 }
1671
1672 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1673 for (const connectorId of this.connectors.keys()) {
1674 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1675 await this.stopTransactionOnConnector(connectorId, reason);
1676 }
1677 }
1678 }
1679
1680 // 0 for disabling
1681 private getConnectionTimeout(): number {
1682 if (
1683 ChargingStationConfigurationUtils.getConfigurationKey(
1684 this,
1685 StandardParametersKey.ConnectionTimeOut
1686 )
1687 ) {
1688 return (
1689 parseInt(
1690 ChargingStationConfigurationUtils.getConfigurationKey(
1691 this,
1692 StandardParametersKey.ConnectionTimeOut
1693 ).value
1694 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1695 );
1696 }
1697 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1698 }
1699
1700 // -1 for unlimited, 0 for disabling
1701 private getAutoReconnectMaxRetries(): number {
1702 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1703 return this.stationInfo.autoReconnectMaxRetries;
1704 }
1705 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1706 return Configuration.getAutoReconnectMaxRetries();
1707 }
1708 return -1;
1709 }
1710
1711 // 0 for disabling
1712 private getRegistrationMaxRetries(): number {
1713 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1714 return this.stationInfo.registrationMaxRetries;
1715 }
1716 return -1;
1717 }
1718
1719 private getPowerDivider(): number {
1720 let powerDivider = this.getNumberOfConnectors();
1721 if (this.stationInfo?.powerSharedByConnectors) {
1722 powerDivider = this.getNumberOfRunningTransactions();
1723 }
1724 return powerDivider;
1725 }
1726
1727 private getMaximumPower(stationInfo?: ChargingStationInfo): number {
1728 const localStationInfo = stationInfo ?? this.stationInfo;
1729 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
1730 }
1731
1732 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1733 const maximumPower = this.getMaximumPower(stationInfo);
1734 switch (this.getCurrentOutType(stationInfo)) {
1735 case CurrentType.AC:
1736 return ACElectricUtils.amperagePerPhaseFromPower(
1737 this.getNumberOfPhases(stationInfo),
1738 maximumPower / this.getNumberOfConnectors(),
1739 this.getVoltageOut(stationInfo)
1740 );
1741 case CurrentType.DC:
1742 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1743 }
1744 }
1745
1746 private getAmperageLimitation(): number | undefined {
1747 if (
1748 this.stationInfo.amperageLimitationOcppKey &&
1749 ChargingStationConfigurationUtils.getConfigurationKey(
1750 this,
1751 this.stationInfo.amperageLimitationOcppKey
1752 )
1753 ) {
1754 return (
1755 Utils.convertToInt(
1756 ChargingStationConfigurationUtils.getConfigurationKey(
1757 this,
1758 this.stationInfo.amperageLimitationOcppKey
1759 ).value
1760 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1761 );
1762 }
1763 }
1764
1765 private getChargingProfilePowerLimit(connectorId: number): number | undefined {
1766 let limit: number, matchingChargingProfile: ChargingProfile;
1767 let chargingProfiles: ChargingProfile[] = [];
1768 // Get charging profiles for connector and sort by stack level
1769 chargingProfiles = this.getConnectorStatus(connectorId).chargingProfiles.sort(
1770 (a, b) => b.stackLevel - a.stackLevel
1771 );
1772 // Get profiles on connector 0
1773 if (this.getConnectorStatus(0).chargingProfiles) {
1774 chargingProfiles.push(
1775 ...this.getConnectorStatus(0).chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
1776 );
1777 }
1778 if (!Utils.isEmptyArray(chargingProfiles)) {
1779 const result = ChargingStationUtils.getLimitFromChargingProfiles(
1780 chargingProfiles,
1781 this.logPrefix()
1782 );
1783 if (!Utils.isNullOrUndefined(result)) {
1784 limit = result.limit;
1785 matchingChargingProfile = result.matchingChargingProfile;
1786 switch (this.getCurrentOutType()) {
1787 case CurrentType.AC:
1788 limit =
1789 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
1790 ChargingRateUnitType.WATT
1791 ? limit
1792 : ACElectricUtils.powerTotal(this.getNumberOfPhases(), this.getVoltageOut(), limit);
1793 break;
1794 case CurrentType.DC:
1795 limit =
1796 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
1797 ChargingRateUnitType.WATT
1798 ? limit
1799 : DCElectricUtils.power(this.getVoltageOut(), limit);
1800 }
1801 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
1802 if (limit > connectorMaximumPower) {
1803 logger.error(
1804 `${this.logPrefix()} Charging profile id ${
1805 matchingChargingProfile.chargingProfileId
1806 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
1807 result
1808 );
1809 limit = connectorMaximumPower;
1810 }
1811 }
1812 }
1813 return limit;
1814 }
1815
1816 private async startMessageSequence(): Promise<void> {
1817 if (this.stationInfo?.autoRegister === true) {
1818 await this.ocppRequestService.requestHandler<
1819 BootNotificationRequest,
1820 BootNotificationResponse
1821 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1822 skipBufferingOnError: true,
1823 });
1824 }
1825 // Start WebSocket ping
1826 this.startWebSocketPing();
1827 // Start heartbeat
1828 this.startHeartbeat();
1829 // Initialize connectors status
1830 for (const connectorId of this.connectors.keys()) {
1831 let connectorStatus: ConnectorStatusEnum;
1832 if (connectorId === 0) {
1833 continue;
1834 } else if (
1835 !this.getConnectorStatus(connectorId)?.status &&
1836 (this.isChargingStationAvailable() === false ||
1837 this.isConnectorAvailable(connectorId) === false)
1838 ) {
1839 connectorStatus = ConnectorStatusEnum.UNAVAILABLE;
1840 } else if (
1841 !this.getConnectorStatus(connectorId)?.status &&
1842 this.getConnectorStatus(connectorId)?.bootStatus
1843 ) {
1844 // Set boot status in template at startup
1845 connectorStatus = this.getConnectorStatus(connectorId).bootStatus;
1846 } else if (this.getConnectorStatus(connectorId)?.status) {
1847 // Set previous status at startup
1848 connectorStatus = this.getConnectorStatus(connectorId).status;
1849 } else {
1850 // Set default status
1851 connectorStatus = ConnectorStatusEnum.AVAILABLE;
1852 }
1853 await this.ocppRequestService.requestHandler<
1854 StatusNotificationRequest,
1855 StatusNotificationResponse
1856 >(
1857 this,
1858 RequestCommand.STATUS_NOTIFICATION,
1859 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1860 );
1861 this.getConnectorStatus(connectorId).status = connectorStatus;
1862 }
1863 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1864 await this.ocppRequestService.requestHandler<
1865 FirmwareStatusNotificationRequest,
1866 FirmwareStatusNotificationResponse
1867 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1868 status: FirmwareStatus.Installed,
1869 });
1870 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
1871 }
1872
1873 // Start the ATG
1874 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
1875 this.startAutomaticTransactionGenerator();
1876 }
1877 this.wsConnectionRestarted === true && this.flushMessageBuffer();
1878 }
1879
1880 private async stopMessageSequence(
1881 reason: StopTransactionReason = StopTransactionReason.NONE
1882 ): Promise<void> {
1883 // Stop WebSocket ping
1884 this.stopWebSocketPing();
1885 // Stop heartbeat
1886 this.stopHeartbeat();
1887 // Stop ongoing transactions
1888 if (this.automaticTransactionGenerator?.started === true) {
1889 this.stopAutomaticTransactionGenerator();
1890 } else {
1891 await this.stopRunningTransactions(reason);
1892 }
1893 for (const connectorId of this.connectors.keys()) {
1894 if (connectorId > 0) {
1895 await this.ocppRequestService.requestHandler<
1896 StatusNotificationRequest,
1897 StatusNotificationResponse
1898 >(
1899 this,
1900 RequestCommand.STATUS_NOTIFICATION,
1901 OCPPServiceUtils.buildStatusNotificationRequest(
1902 this,
1903 connectorId,
1904 ConnectorStatusEnum.UNAVAILABLE
1905 )
1906 );
1907 this.getConnectorStatus(connectorId).status = null;
1908 }
1909 }
1910 }
1911
1912 private startWebSocketPing(): void {
1913 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1914 this,
1915 StandardParametersKey.WebSocketPingInterval
1916 )
1917 ? Utils.convertToInt(
1918 ChargingStationConfigurationUtils.getConfigurationKey(
1919 this,
1920 StandardParametersKey.WebSocketPingInterval
1921 ).value
1922 )
1923 : 0;
1924 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1925 this.webSocketPingSetInterval = setInterval(() => {
1926 if (this.isWebSocketConnectionOpened() === true) {
1927 this.wsConnection.ping();
1928 }
1929 }, webSocketPingInterval * 1000);
1930 logger.info(
1931 this.logPrefix() +
1932 ' WebSocket ping started every ' +
1933 Utils.formatDurationSeconds(webSocketPingInterval)
1934 );
1935 } else if (this.webSocketPingSetInterval) {
1936 logger.info(
1937 this.logPrefix() +
1938 ' WebSocket ping already started every ' +
1939 Utils.formatDurationSeconds(webSocketPingInterval)
1940 );
1941 } else {
1942 logger.error(
1943 `${this.logPrefix()} WebSocket ping interval set to ${
1944 webSocketPingInterval
1945 ? Utils.formatDurationSeconds(webSocketPingInterval)
1946 : webSocketPingInterval
1947 }, not starting the WebSocket ping`
1948 );
1949 }
1950 }
1951
1952 private stopWebSocketPing(): void {
1953 if (this.webSocketPingSetInterval) {
1954 clearInterval(this.webSocketPingSetInterval);
1955 }
1956 }
1957
1958 private getConfiguredSupervisionUrl(): URL {
1959 const supervisionUrls = this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls();
1960 if (!Utils.isEmptyArray(supervisionUrls)) {
1961 switch (Configuration.getSupervisionUrlDistribution()) {
1962 case SupervisionUrlDistribution.ROUND_ROBIN:
1963 // FIXME
1964 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1965 break;
1966 case SupervisionUrlDistribution.RANDOM:
1967 this.configuredSupervisionUrlIndex = Math.floor(
1968 Utils.secureRandom() * supervisionUrls.length
1969 );
1970 break;
1971 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1972 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1973 break;
1974 default:
1975 logger.error(
1976 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1977 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
1978 }`
1979 );
1980 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
1981 break;
1982 }
1983 return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
1984 }
1985 return new URL(supervisionUrls as string);
1986 }
1987
1988 private getHeartbeatInterval(): number {
1989 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1990 this,
1991 StandardParametersKey.HeartbeatInterval
1992 );
1993 if (HeartbeatInterval) {
1994 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1995 }
1996 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1997 this,
1998 StandardParametersKey.HeartBeatInterval
1999 );
2000 if (HeartBeatInterval) {
2001 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
2002 }
2003 this.stationInfo?.autoRegister === false &&
2004 logger.warn(
2005 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
2006 Constants.DEFAULT_HEARTBEAT_INTERVAL
2007 }`
2008 );
2009 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
2010 }
2011
2012 private stopHeartbeat(): void {
2013 if (this.heartbeatSetInterval) {
2014 clearInterval(this.heartbeatSetInterval);
2015 }
2016 }
2017
2018 private terminateWSConnection(): void {
2019 if (this.isWebSocketConnectionOpened() === true) {
2020 this.wsConnection.terminate();
2021 this.wsConnection = null;
2022 }
2023 }
2024
2025 private stopMeterValues(connectorId: number) {
2026 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
2027 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
2028 }
2029 }
2030
2031 private getReconnectExponentialDelay(): boolean {
2032 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
2033 ? this.stationInfo.reconnectExponentialDelay
2034 : false;
2035 }
2036
2037 private async reconnect(): Promise<void> {
2038 // Stop WebSocket ping
2039 this.stopWebSocketPing();
2040 // Stop heartbeat
2041 this.stopHeartbeat();
2042 // Stop the ATG if needed
2043 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
2044 this.stopAutomaticTransactionGenerator();
2045 }
2046 if (
2047 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2048 this.getAutoReconnectMaxRetries() === -1
2049 ) {
2050 this.autoReconnectRetryCount++;
2051 const reconnectDelay = this.getReconnectExponentialDelay()
2052 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2053 : this.getConnectionTimeout() * 1000;
2054 const reconnectDelayWithdraw = 1000;
2055 const reconnectTimeout =
2056 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2057 ? reconnectDelay - reconnectDelayWithdraw
2058 : 0;
2059 logger.error(
2060 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2061 reconnectDelay,
2062 2
2063 )}ms, timeout ${reconnectTimeout}ms`
2064 );
2065 await Utils.sleep(reconnectDelay);
2066 logger.error(
2067 this.logPrefix() + ' WebSocket connection retry #' + this.autoReconnectRetryCount.toString()
2068 );
2069 this.openWSConnection(
2070 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
2071 { closeOpened: true }
2072 );
2073 this.wsConnectionRestarted = true;
2074 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2075 logger.error(
2076 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2077 this.autoReconnectRetryCount
2078 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
2079 );
2080 }
2081 }
2082
2083 private getAutomaticTransactionGeneratorConfigurationFromTemplate(): AutomaticTransactionGeneratorConfiguration | null {
2084 return this.getTemplateFromFile()?.AutomaticTransactionGenerator ?? null;
2085 }
2086
2087 private initializeConnectorStatus(connectorId: number): void {
2088 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2089 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2090 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
2091 this.getConnectorStatus(connectorId).transactionStarted = false;
2092 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2093 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
2094 }
2095 }