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