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