1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
3 import { ACElectricUtils
, DCElectricUtils
} from
'../utils/ElectricUtils';
6 BootNotificationRequest
,
10 IncomingRequestCommand
,
13 StatusNotificationRequest
,
14 } from
'../types/ocpp/Requests';
16 BootNotificationResponse
,
22 StatusNotificationResponse
,
23 } from
'../types/ocpp/Responses';
27 ChargingSchedulePeriod
,
28 } from
'../types/ocpp/ChargingProfile';
29 import ChargingStationConfiguration
, { Section
} from
'../types/ChargingStationConfiguration';
30 import ChargingStationOcppConfiguration
, {
32 } from
'../types/ChargingStationOcppConfiguration';
33 import ChargingStationTemplate
, {
39 } from
'../types/ChargingStationTemplate';
41 ConnectorPhaseRotation
,
42 StandardParametersKey
,
43 SupportedFeatureProfiles
,
44 VendorDefaultParametersKey
,
45 } from
'../types/ocpp/Configuration';
46 import { MeterValue
, MeterValueMeasurand
, MeterValuePhase
} from
'../types/ocpp/MeterValues';
48 StopTransactionReason
,
49 StopTransactionRequest
,
50 StopTransactionResponse
,
51 } from
'../types/ocpp/Transaction';
52 import { WSError
, WebSocketCloseEventStatusCode
} from
'../types/WebSocket';
53 import WebSocket
, { Data
, OPEN
, RawData
} from
'ws';
55 import AutomaticTransactionGenerator from
'./AutomaticTransactionGenerator';
56 import BaseError from
'../exception/BaseError';
57 import { ChargePointErrorCode
} from
'../types/ocpp/ChargePointErrorCode';
58 import { ChargePointStatus
} from
'../types/ocpp/ChargePointStatus';
59 import ChargingStationInfo from
'../types/ChargingStationInfo';
60 import { ChargingStationWorkerMessageEvents
} from
'../types/ChargingStationWorker';
61 import Configuration from
'../utils/Configuration';
62 import { ConnectorStatus
} from
'../types/ConnectorStatus';
63 import Constants from
'../utils/Constants';
64 import { ErrorType
} from
'../types/ocpp/ErrorType';
65 import { FileType
} from
'../types/FileType';
66 import FileUtils from
'../utils/FileUtils';
67 import { JsonType
} from
'../types/JsonType';
68 import { MessageType
} from
'../types/ocpp/MessageType';
69 import OCPP16IncomingRequestService from
'./ocpp/1.6/OCPP16IncomingRequestService';
70 import OCPP16RequestService from
'./ocpp/1.6/OCPP16RequestService';
71 import OCPP16ResponseService from
'./ocpp/1.6/OCPP16ResponseService';
72 import { OCPP16ServiceUtils
} from
'./ocpp/1.6/OCPP16ServiceUtils';
73 import OCPPError from
'../exception/OCPPError';
74 import OCPPIncomingRequestService from
'./ocpp/OCPPIncomingRequestService';
75 import OCPPRequestService from
'./ocpp/OCPPRequestService';
76 import { OCPPVersion
} from
'../types/ocpp/OCPPVersion';
77 import PerformanceStatistics from
'../performance/PerformanceStatistics';
78 import { SampledValueTemplate
} from
'../types/MeasurandPerPhaseSampledValueTemplates';
79 import { SupervisionUrlDistribution
} from
'../types/ConfigurationData';
80 import { URL
} from
'url';
81 import Utils from
'../utils/Utils';
82 import crypto from
'crypto';
84 import logger from
'../utils/Logger';
85 import { parentPort
} from
'worker_threads';
86 import path from
'path';
88 export default class ChargingStation
{
89 public hashId
!: string;
90 public readonly templateFile
: string;
91 public authorizedTags
: string[];
92 public stationInfo
!: ChargingStationInfo
;
93 public readonly connectors
: Map
<number, ConnectorStatus
>;
94 public ocppConfiguration
!: ChargingStationOcppConfiguration
;
95 public wsConnection
!: WebSocket
;
96 public readonly requests
: Map
<string, CachedRequest
>;
97 public performanceStatistics
!: PerformanceStatistics
;
98 public heartbeatSetInterval
!: NodeJS
.Timeout
;
99 public ocppRequestService
!: OCPPRequestService
;
100 public bootNotificationResponse
!: BootNotificationResponse
| null;
101 private readonly index
: number;
102 private configurationFile
!: string;
103 private bootNotificationRequest
!: BootNotificationRequest
;
104 private connectorsConfigurationHash
!: string;
105 private ocppIncomingRequestService
!: OCPPIncomingRequestService
;
106 private readonly messageBuffer
: Set
<string>;
107 private wsConfiguredConnectionUrl
!: URL
;
108 private wsConnectionRestarted
: boolean;
109 private stopped
: boolean;
110 private autoReconnectRetryCount
: number;
111 private automaticTransactionGenerator
!: AutomaticTransactionGenerator
;
112 private webSocketPingSetInterval
!: NodeJS
.Timeout
;
114 constructor(index
: number, templateFile
: string) {
116 this.templateFile
= templateFile
;
117 this.stopped
= false;
118 this.wsConnectionRestarted
= false;
119 this.autoReconnectRetryCount
= 0;
120 this.connectors
= new Map
<number, ConnectorStatus
>();
121 this.requests
= new Map
<string, CachedRequest
>();
122 this.messageBuffer
= new Set
<string>();
124 this.authorizedTags
= this.getAuthorizedTags();
127 private get
wsConnectionUrl(): URL
{
128 return this.getSupervisionUrlOcppConfiguration()
130 this.getConfigurationKey(this.getSupervisionUrlOcppKey()).value
+
132 this.stationInfo
.chargingStationId
134 : this.wsConfiguredConnectionUrl
;
137 public logPrefix(): string {
138 return Utils
.logPrefix(` ${this.stationInfo.chargingStationId} |`);
141 public getBootNotificationRequest(): BootNotificationRequest
{
142 return this.bootNotificationRequest
;
145 public getRandomIdTag(): string {
146 const index
= Math.floor(Utils
.secureRandom() * this.authorizedTags
.length
);
147 return this.authorizedTags
[index
];
150 public hasAuthorizedTags(): boolean {
151 return !Utils
.isEmptyArray(this.authorizedTags
);
154 public getEnableStatistics(): boolean | undefined {
155 return !Utils
.isUndefined(this.stationInfo
.enableStatistics
)
156 ? this.stationInfo
.enableStatistics
160 public getMayAuthorizeAtRemoteStart(): boolean | undefined {
161 return this.stationInfo
.mayAuthorizeAtRemoteStart
?? true;
164 public getNumberOfPhases(): number | undefined {
165 switch (this.getCurrentOutType()) {
167 return !Utils
.isUndefined(this.stationInfo
.numberOfPhases
)
168 ? this.stationInfo
.numberOfPhases
175 public isWebSocketConnectionOpened(): boolean {
176 return this?.wsConnection
?.readyState
=== OPEN
;
179 public getRegistrationStatus(): RegistrationStatus
{
180 return this?.bootNotificationResponse
?.status;
183 public isInUnknownState(): boolean {
184 return Utils
.isNullOrUndefined(this?.bootNotificationResponse
?.status);
187 public isInPendingState(): boolean {
188 return this?.bootNotificationResponse
?.status === RegistrationStatus
.PENDING
;
191 public isInAcceptedState(): boolean {
192 return this?.bootNotificationResponse
?.status === RegistrationStatus
.ACCEPTED
;
195 public isInRejectedState(): boolean {
196 return this?.bootNotificationResponse
?.status === RegistrationStatus
.REJECTED
;
199 public isRegistered(): boolean {
200 return !this.isInUnknownState() && (this.isInAcceptedState() || this.isInPendingState());
203 public isChargingStationAvailable(): boolean {
204 return this.getConnectorStatus(0).availability
=== AvailabilityType
.OPERATIVE
;
207 public isConnectorAvailable(id
: number): boolean {
208 return id
> 0 && this.getConnectorStatus(id
).availability
=== AvailabilityType
.OPERATIVE
;
211 public getNumberOfConnectors(): number {
212 return this.connectors
.get(0) ? this.connectors
.size
- 1 : this.connectors
.size
;
215 public getConnectorStatus(id
: number): ConnectorStatus
{
216 return this.connectors
.get(id
);
219 public getCurrentOutType(): CurrentType
| undefined {
220 return this.stationInfo
.currentOutType
?? CurrentType
.AC
;
223 public getOcppStrictCompliance(): boolean {
224 return this.stationInfo
.ocppStrictCompliance
?? false;
227 public getVoltageOut(): number | undefined {
228 const errMsg
= `${this.logPrefix()} Unknown ${this.getCurrentOutType()} currentOutType in template file ${
230 }, cannot define default voltage out`;
231 let defaultVoltageOut
: number;
232 switch (this.getCurrentOutType()) {
234 defaultVoltageOut
= Voltage
.VOLTAGE_230
;
237 defaultVoltageOut
= Voltage
.VOLTAGE_400
;
240 logger
.error(errMsg
);
241 throw new Error(errMsg
);
243 return !Utils
.isUndefined(this.stationInfo
.voltageOut
)
244 ? this.stationInfo
.voltageOut
248 public getConnectorMaximumAvailablePower(connectorId
: number): number {
249 let connectorAmperageLimitationPowerLimit
: number;
251 !Utils
.isNullOrUndefined(this.getAmperageLimitation()) &&
252 this.getAmperageLimitation() < this.stationInfo
.maximumAmperage
254 connectorAmperageLimitationPowerLimit
=
255 (this.getCurrentOutType() === CurrentType
.AC
256 ? ACElectricUtils
.powerTotal(
257 this.getNumberOfPhases(),
258 this.getVoltageOut(),
259 this.getAmperageLimitation() * this.getNumberOfConnectors()
261 : DCElectricUtils
.power(this.getVoltageOut(), this.getAmperageLimitation())) /
262 this.stationInfo
.powerDivider
;
264 const connectorMaximumPower
= this.getMaximumPower() / this.stationInfo
.powerDivider
;
265 const connectorChargingProfilePowerLimit
= this.getChargingProfilePowerLimit(connectorId
);
267 isNaN(connectorMaximumPower
) ? Infinity : connectorMaximumPower
,
268 isNaN(connectorAmperageLimitationPowerLimit
)
270 : connectorAmperageLimitationPowerLimit
,
271 isNaN(connectorChargingProfilePowerLimit
) ? Infinity : connectorChargingProfilePowerLimit
275 public getTransactionIdTag(transactionId
: number): string | undefined {
276 for (const connectorId
of this.connectors
.keys()) {
277 if (connectorId
> 0 && this.getConnectorStatus(connectorId
).transactionId
=== transactionId
) {
278 return this.getConnectorStatus(connectorId
).transactionIdTag
;
283 public getOutOfOrderEndMeterValues(): boolean {
284 return this.stationInfo
.outOfOrderEndMeterValues
?? false;
287 public getBeginEndMeterValues(): boolean {
288 return this.stationInfo
.beginEndMeterValues
?? false;
291 public getMeteringPerTransaction(): boolean {
292 return this.stationInfo
.meteringPerTransaction
?? true;
295 public getTransactionDataMeterValues(): boolean {
296 return this.stationInfo
.transactionDataMeterValues
?? false;
299 public getMainVoltageMeterValues(): boolean {
300 return this.stationInfo
.mainVoltageMeterValues
?? true;
303 public getPhaseLineToLineVoltageMeterValues(): boolean {
304 return this.stationInfo
.phaseLineToLineVoltageMeterValues
?? false;
307 public getConnectorIdByTransactionId(transactionId
: number): number | undefined {
308 for (const connectorId
of this.connectors
.keys()) {
311 this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
318 public getEnergyActiveImportRegisterByTransactionId(transactionId
: number): number | undefined {
319 const transactionConnectorStatus
= this.getConnectorStatus(
320 this.getConnectorIdByTransactionId(transactionId
)
322 if (this.getMeteringPerTransaction()) {
323 return transactionConnectorStatus
?.transactionEnergyActiveImportRegisterValue
;
325 return transactionConnectorStatus
?.energyActiveImportRegisterValue
;
328 public getEnergyActiveImportRegisterByConnectorId(connectorId
: number): number | undefined {
329 const connectorStatus
= this.getConnectorStatus(connectorId
);
330 if (this.getMeteringPerTransaction()) {
331 return connectorStatus
?.transactionEnergyActiveImportRegisterValue
;
333 return connectorStatus
?.energyActiveImportRegisterValue
;
336 public getAuthorizeRemoteTxRequests(): boolean {
337 const authorizeRemoteTxRequests
= this.getConfigurationKey(
338 StandardParametersKey
.AuthorizeRemoteTxRequests
340 return authorizeRemoteTxRequests
341 ? Utils
.convertToBoolean(authorizeRemoteTxRequests
.value
)
345 public getLocalAuthListEnabled(): boolean {
346 const localAuthListEnabled
= this.getConfigurationKey(
347 StandardParametersKey
.LocalAuthListEnabled
349 return localAuthListEnabled
? Utils
.convertToBoolean(localAuthListEnabled
.value
) : false;
352 public restartWebSocketPing(): void {
353 // Stop WebSocket ping
354 this.stopWebSocketPing();
355 // Start WebSocket ping
356 this.startWebSocketPing();
359 public getSampledValueTemplate(
361 measurand
: MeterValueMeasurand
= MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
362 phase
?: MeterValuePhase
363 ): SampledValueTemplate
| undefined {
364 const onPhaseStr
= phase
? `on phase ${phase} ` : '';
365 if (!Constants
.SUPPORTED_MEASURANDS
.includes(measurand
)) {
367 `${this.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
372 measurand
!== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
373 !this.getConfigurationKey(StandardParametersKey
.MeterValuesSampledData
)?.value
.includes(
378 `${this.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
379 StandardParametersKey.MeterValuesSampledData
384 const sampledValueTemplates
: SampledValueTemplate
[] =
385 this.getConnectorStatus(connectorId
).MeterValues
;
388 !Utils
.isEmptyArray(sampledValueTemplates
) && index
< sampledValueTemplates
.length
;
392 !Constants
.SUPPORTED_MEASURANDS
.includes(
393 sampledValueTemplates
[index
]?.measurand
??
394 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
398 `${this.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
402 sampledValueTemplates
[index
]?.phase
=== phase
&&
403 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
404 this.getConfigurationKey(StandardParametersKey
.MeterValuesSampledData
)?.value
.includes(
408 return sampledValueTemplates
[index
];
411 !sampledValueTemplates
[index
].phase
&&
412 sampledValueTemplates
[index
]?.measurand
=== measurand
&&
413 this.getConfigurationKey(StandardParametersKey
.MeterValuesSampledData
)?.value
.includes(
417 return sampledValueTemplates
[index
];
419 measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
&&
420 (!sampledValueTemplates
[index
].measurand
||
421 sampledValueTemplates
[index
].measurand
=== measurand
)
423 return sampledValueTemplates
[index
];
426 if (measurand
=== MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
) {
427 const errorMsg
= `${this.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
428 logger
.error(errorMsg
);
429 throw new Error(errorMsg
);
432 `${this.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
436 public getAutomaticTransactionGeneratorRequireAuthorize(): boolean {
437 return this.stationInfo
.AutomaticTransactionGenerator
.requireAuthorize
?? true;
440 public startHeartbeat(): void {
442 this.getHeartbeatInterval() &&
443 this.getHeartbeatInterval() > 0 &&
444 !this.heartbeatSetInterval
446 // eslint-disable-next-line @typescript-eslint/no-misused-promises
447 this.heartbeatSetInterval
= setInterval(async (): Promise
<void> => {
448 await this.ocppRequestService
.requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(
449 RequestCommand
.HEARTBEAT
451 }, this.getHeartbeatInterval());
454 ' Heartbeat started every ' +
455 Utils
.formatDurationMilliSeconds(this.getHeartbeatInterval())
457 } else if (this.heartbeatSetInterval
) {
460 ' Heartbeat already started every ' +
461 Utils
.formatDurationMilliSeconds(this.getHeartbeatInterval())
465 `${this.logPrefix()} Heartbeat interval set to ${
466 this.getHeartbeatInterval()
467 ? Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
468 : this.getHeartbeatInterval()
469 }, not starting the heartbeat`
474 public restartHeartbeat(): void {
476 this.stopHeartbeat();
478 this.startHeartbeat();
481 public startMeterValues(connectorId
: number, interval
: number): void {
482 if (connectorId
=== 0) {
484 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
488 if (!this.getConnectorStatus(connectorId
)) {
490 `${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`
494 if (!this.getConnectorStatus(connectorId
)?.transactionStarted
) {
496 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
500 this.getConnectorStatus(connectorId
)?.transactionStarted
&&
501 !this.getConnectorStatus(connectorId
)?.transactionId
504 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`
509 // eslint-disable-next-line @typescript-eslint/no-misused-promises
510 this.getConnectorStatus(connectorId
).transactionSetInterval
= setInterval(
511 // eslint-disable-next-line @typescript-eslint/no-misused-promises
512 async (): Promise
<void> => {
513 // FIXME: Implement OCPP version agnostic helpers
514 const meterValue
: MeterValue
= OCPP16ServiceUtils
.buildMeterValue(
517 this.getConnectorStatus(connectorId
).transactionId
,
520 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
521 RequestCommand
.METER_VALUES
,
524 transactionId
: this.getConnectorStatus(connectorId
).transactionId
,
525 meterValue
: [meterValue
],
533 `${this.logPrefix()} Charging station ${
534 StandardParametersKey.MeterValueSampleInterval
535 } configuration set to ${
536 interval ? Utils.formatDurationMilliSeconds(interval) : interval
537 }, not sending MeterValues`
542 public start(): void {
543 if (this.getEnableStatistics()) {
544 this.performanceStatistics
.start();
546 this.openWSConnection();
547 // Handle WebSocket message
548 this.wsConnection
.on(
550 this.onMessage
.bind(this) as (this: WebSocket
, data
: RawData
, isBinary
: boolean) => void
552 // Handle WebSocket error
553 this.wsConnection
.on(
555 this.onError
.bind(this) as (this: WebSocket
, error
: Error) => void
557 // Handle WebSocket close
558 this.wsConnection
.on(
560 this.onClose
.bind(this) as (this: WebSocket
, code
: number, reason
: Buffer
) => void
562 // Handle WebSocket open
563 this.wsConnection
.on('open', this.onOpen
.bind(this) as (this: WebSocket
) => void);
564 // Handle WebSocket ping
565 this.wsConnection
.on('ping', this.onPing
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
566 // Handle WebSocket pong
567 this.wsConnection
.on('pong', this.onPong
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
568 // Monitor authorization file
569 FileUtils
.watchJsonFile
<string[]>(
571 FileType
.Authorization
,
572 this.getAuthorizationFile(),
575 // Monitor charging station template file
576 FileUtils
.watchJsonFile(
578 FileType
.ChargingStationTemplate
,
581 (event
, filename
): void => {
582 if (filename
&& event
=== 'change') {
585 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
587 } file have changed, reload`
593 !this.stationInfo
.AutomaticTransactionGenerator
.enable
&&
594 this.automaticTransactionGenerator
596 this.automaticTransactionGenerator
.stop();
598 this.startAutomaticTransactionGenerator();
599 if (this.getEnableStatistics()) {
600 this.performanceStatistics
.restart();
602 this.performanceStatistics
.stop();
604 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
607 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error: %j`,
614 parentPort
.postMessage({
615 id
: ChargingStationWorkerMessageEvents
.STARTED
,
616 data
: { id
: this.stationInfo
.chargingStationId
},
620 public async stop(reason
: StopTransactionReason
= StopTransactionReason
.NONE
): Promise
<void> {
621 // Stop message sequence
622 await this.stopMessageSequence(reason
);
623 for (const connectorId
of this.connectors
.keys()) {
624 if (connectorId
> 0) {
625 await this.ocppRequestService
.requestHandler
<
626 StatusNotificationRequest
,
627 StatusNotificationResponse
628 >(RequestCommand
.STATUS_NOTIFICATION
, {
630 status: ChargePointStatus
.UNAVAILABLE
,
631 errorCode
: ChargePointErrorCode
.NO_ERROR
,
633 this.getConnectorStatus(connectorId
).status = ChargePointStatus
.UNAVAILABLE
;
636 if (this.isWebSocketConnectionOpened()) {
637 this.wsConnection
.close();
639 if (this.getEnableStatistics()) {
640 this.performanceStatistics
.stop();
642 this.bootNotificationResponse
= null;
643 parentPort
.postMessage({
644 id
: ChargingStationWorkerMessageEvents
.STOPPED
,
645 data
: { id
: this.stationInfo
.chargingStationId
},
650 public async reset(reason
?: StopTransactionReason
): Promise
<void> {
651 await this.stop(reason
);
652 await Utils
.sleep(this.stationInfo
.resetTime
);
653 this.stationInfo
= this.getStationInfo();
654 this.stationInfo
?.Connectors
&& delete this.stationInfo
.Connectors
;
658 public getConfigurationKey(
659 key
: string | StandardParametersKey
,
660 caseInsensitive
= false
661 ): ConfigurationKey
| undefined {
662 return this.ocppConfiguration
.configurationKey
.find((configElement
) => {
663 if (caseInsensitive
) {
664 return configElement
.key
.toLowerCase() === key
.toLowerCase();
666 return configElement
.key
=== key
;
670 public addConfigurationKey(
671 key
: string | StandardParametersKey
,
673 options
: { readonly?: boolean; visible
?: boolean; reboot
?: boolean } = {
678 params
: { overwrite
?: boolean; save
?: boolean } = { overwrite
: false, save
: false }
680 options
= options
?? ({} as { readonly?: boolean; visible
?: boolean; reboot
?: boolean });
681 options
.readonly = options
?.readonly ?? false;
682 options
.visible
= options
?.visible
?? true;
683 options
.reboot
= options
?.reboot
?? false;
684 let keyFound
= this.getConfigurationKey(key
);
685 if (keyFound
&& params
?.overwrite
) {
686 this.deleteConfigurationKey(keyFound
.key
, { save
: false });
687 keyFound
= undefined;
690 this.ocppConfiguration
.configurationKey
.push({
692 readonly: options
.readonly,
694 visible
: options
.visible
,
695 reboot
: options
.reboot
,
697 params
?.save
&& this.saveOcppConfiguration();
700 `${this.logPrefix()} Trying to add an already existing configuration key: %j`,
706 public setConfigurationKeyValue(
707 key
: string | StandardParametersKey
,
709 caseInsensitive
= false
711 const keyFound
= this.getConfigurationKey(key
, caseInsensitive
);
713 this.ocppConfiguration
.configurationKey
[
714 this.ocppConfiguration
.configurationKey
.indexOf(keyFound
)
716 this.saveOcppConfiguration();
719 `${this.logPrefix()} Trying to set a value on a non existing configuration key: %j`,
725 public deleteConfigurationKey(
726 key
: string | StandardParametersKey
,
727 params
: { save
?: boolean; caseInsensitive
?: boolean } = { save
: true, caseInsensitive
: false }
728 ): ConfigurationKey
[] {
729 const keyFound
= this.getConfigurationKey(key
, params
?.caseInsensitive
);
731 const deletedConfigurationKey
= this.ocppConfiguration
.configurationKey
.splice(
732 this.ocppConfiguration
.configurationKey
.indexOf(keyFound
),
735 params
?.save
&& this.saveOcppConfiguration();
736 return deletedConfigurationKey
;
740 public getChargingProfilePowerLimit(connectorId
: number): number | undefined {
741 const timestamp
= new Date().getTime();
742 let matchingChargingProfile
: ChargingProfile
;
743 let chargingSchedulePeriods
: ChargingSchedulePeriod
[] = [];
744 if (!Utils
.isEmptyArray(this.getConnectorStatus(connectorId
)?.chargingProfiles
)) {
745 const chargingProfiles
: ChargingProfile
[] = this.getConnectorStatus(
747 ).chargingProfiles
.filter(
749 timestamp
>= chargingProfile
.chargingSchedule
?.startSchedule
.getTime() &&
751 chargingProfile
.chargingSchedule
?.startSchedule
.getTime() +
752 chargingProfile
.chargingSchedule
.duration
* 1000 &&
753 chargingProfile
?.stackLevel
=== Math.max(...chargingProfiles
.map((cp
) => cp
?.stackLevel
))
755 if (!Utils
.isEmptyArray(chargingProfiles
)) {
756 for (const chargingProfile
of chargingProfiles
) {
757 if (!Utils
.isEmptyArray(chargingProfile
.chargingSchedule
.chargingSchedulePeriod
)) {
758 chargingSchedulePeriods
=
759 chargingProfile
.chargingSchedule
.chargingSchedulePeriod
.filter(
760 (chargingSchedulePeriod
, index
) => {
762 chargingProfile
.chargingSchedule
.startSchedule
.getTime() +
763 chargingSchedulePeriod
.startPeriod
* 1000 &&
764 ((chargingProfile
.chargingSchedule
.chargingSchedulePeriod
[index
+ 1] &&
766 chargingProfile
.chargingSchedule
.startSchedule
.getTime() +
767 chargingProfile
.chargingSchedule
.chargingSchedulePeriod
[index
+ 1]
770 !chargingProfile
.chargingSchedule
.chargingSchedulePeriod
[index
+ 1]);
773 if (!Utils
.isEmptyArray(chargingSchedulePeriods
)) {
774 matchingChargingProfile
= chargingProfile
;
782 if (!Utils
.isEmptyArray(chargingSchedulePeriods
)) {
783 switch (this.getCurrentOutType()) {
786 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
787 ? chargingSchedulePeriods
[0].limit
788 : ACElectricUtils
.powerTotal(
789 this.getNumberOfPhases(),
790 this.getVoltageOut(),
791 chargingSchedulePeriods
[0].limit
796 matchingChargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
797 ? chargingSchedulePeriods
[0].limit
798 : DCElectricUtils
.power(this.getVoltageOut(), chargingSchedulePeriods
[0].limit
);
801 const connectorMaximumPower
= this.getMaximumPower() / this.stationInfo
.powerDivider
;
802 if (limit
> connectorMaximumPower
) {
804 `${this.logPrefix()} Charging profile id ${
805 matchingChargingProfile.chargingProfileId
806 } limit is greater than connector id ${connectorId} maximum, dump charging profiles' stack: %j`,
807 this.getConnectorStatus(connectorId
).chargingProfiles
809 limit
= connectorMaximumPower
;
814 public setChargingProfile(connectorId
: number, cp
: ChargingProfile
): void {
815 let cpReplaced
= false;
816 if (!Utils
.isEmptyArray(this.getConnectorStatus(connectorId
).chargingProfiles
)) {
817 this.getConnectorStatus(connectorId
).chargingProfiles
?.forEach(
818 (chargingProfile
: ChargingProfile
, index
: number) => {
820 chargingProfile
.chargingProfileId
=== cp
.chargingProfileId
||
821 (chargingProfile
.stackLevel
=== cp
.stackLevel
&&
822 chargingProfile
.chargingProfilePurpose
=== cp
.chargingProfilePurpose
)
824 this.getConnectorStatus(connectorId
).chargingProfiles
[index
] = cp
;
830 !cpReplaced
&& this.getConnectorStatus(connectorId
).chargingProfiles
?.push(cp
);
833 public resetConnectorStatus(connectorId
: number): void {
834 this.getConnectorStatus(connectorId
).idTagLocalAuthorized
= false;
835 this.getConnectorStatus(connectorId
).idTagAuthorized
= false;
836 this.getConnectorStatus(connectorId
).transactionRemoteStarted
= false;
837 this.getConnectorStatus(connectorId
).transactionStarted
= false;
838 delete this.getConnectorStatus(connectorId
).localAuthorizeIdTag
;
839 delete this.getConnectorStatus(connectorId
).authorizeIdTag
;
840 delete this.getConnectorStatus(connectorId
).transactionId
;
841 delete this.getConnectorStatus(connectorId
).transactionIdTag
;
842 this.getConnectorStatus(connectorId
).transactionEnergyActiveImportRegisterValue
= 0;
843 delete this.getConnectorStatus(connectorId
).transactionBeginMeterValue
;
844 this.stopMeterValues(connectorId
);
847 public hasFeatureProfile(featureProfile
: SupportedFeatureProfiles
) {
848 return this.getConfigurationKey(StandardParametersKey
.SupportedFeatureProfiles
)?.value
.includes(
853 public bufferMessage(message
: string): void {
854 this.messageBuffer
.add(message
);
857 private flushMessageBuffer() {
858 if (this.messageBuffer
.size
> 0) {
859 this.messageBuffer
.forEach((message
) => {
860 // TODO: evaluate the need to track performance
861 this.wsConnection
.send(message
);
862 this.messageBuffer
.delete(message
);
867 private getSupervisionUrlOcppConfiguration(): boolean {
868 return this.stationInfo
.supervisionUrlOcppConfiguration
?? false;
871 private getSupervisionUrlOcppKey(): string {
872 return this.stationInfo
.supervisionUrlOcppKey
?? VendorDefaultParametersKey
.ConnectionUrl
;
875 private getChargingStationId(stationTemplate
: ChargingStationTemplate
): string {
876 // In case of multiple instances: add instance index to charging station id
877 const instanceIndex
= process
.env
.CF_INSTANCE_INDEX
?? 0;
878 const idSuffix
= stationTemplate
.nameSuffix
?? '';
879 const idStr
= '000000000' + this.index
.toString();
880 return stationTemplate
.fixedName
881 ? stationTemplate
.baseName
882 : stationTemplate
.baseName
+
884 instanceIndex
.toString() +
885 idStr
.substring(idStr
.length
- 4) +
889 private getRandomSerialNumberSuffix(params
?: {
890 randomBytesLength
?: number;
893 const randomSerialNumberSuffix
= crypto
894 .randomBytes(params
?.randomBytesLength
?? 16)
896 if (params
?.upperCase
) {
897 return randomSerialNumberSuffix
.toUpperCase();
899 return randomSerialNumberSuffix
;
902 private getTemplateFromFile(): ChargingStationTemplate
| null {
903 let template
: ChargingStationTemplate
= null;
905 const measureId
= `${FileType.ChargingStationTemplate} read`;
906 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
908 (JSON
.parse(fs
.readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
) ??
909 ({} as ChargingStationTemplate
);
910 PerformanceStatistics
.endMeasure(measureId
, beginId
);
911 template
.templateHash
= crypto
912 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
913 .update(JSON
.stringify(template
))
916 FileUtils
.handleFileException(
918 FileType
.ChargingStationTemplate
,
920 error
as NodeJS
.ErrnoException
926 private createSerialNumber(
927 stationInfo
: ChargingStationInfo
,
928 existingStationInfo
?: ChargingStationInfo
,
929 params
: { randomSerialNumberUpperCase
?: boolean; randomSerialNumber
?: boolean } = {
930 randomSerialNumberUpperCase
: true,
931 randomSerialNumber
: true,
934 params
= params
?? {};
935 params
.randomSerialNumberUpperCase
= params
?.randomSerialNumberUpperCase
?? true;
936 params
.randomSerialNumber
= params
?.randomSerialNumber
?? true;
937 if (!Utils
.isEmptyObject(existingStationInfo
)) {
938 existingStationInfo
?.chargePointSerialNumber
&&
939 (stationInfo
.chargePointSerialNumber
= existingStationInfo
.chargePointSerialNumber
);
940 existingStationInfo
?.chargeBoxSerialNumber
&&
941 (stationInfo
.chargeBoxSerialNumber
= existingStationInfo
.chargeBoxSerialNumber
);
942 existingStationInfo
?.meterSerialNumber
&&
943 (stationInfo
.meterSerialNumber
= existingStationInfo
.meterSerialNumber
);
945 const serialNumberSuffix
= params
?.randomSerialNumber
946 ? this.getRandomSerialNumberSuffix({ upperCase
: params
.randomSerialNumberUpperCase
})
948 stationInfo
.chargePointSerialNumber
=
949 stationInfo
?.chargePointSerialNumberPrefix
&&
950 stationInfo
.chargePointSerialNumberPrefix
+ serialNumberSuffix
;
951 stationInfo
.chargeBoxSerialNumber
=
952 stationInfo
?.chargeBoxSerialNumberPrefix
&&
953 stationInfo
.chargeBoxSerialNumberPrefix
+ serialNumberSuffix
;
954 stationInfo
.meterSerialNumber
=
955 stationInfo
?.meterSerialNumberPrefix
&&
956 stationInfo
.meterSerialNumberPrefix
+ serialNumberSuffix
;
960 private getStationInfoFromTemplate(): ChargingStationInfo
{
961 const stationInfo
: ChargingStationInfo
= this.getTemplateFromFile();
962 if (Utils
.isNullOrUndefined(stationInfo
)) {
963 const logMsg
= 'Failed to read charging station template file';
964 logger
.error(`${this.logPrefix()} ${logMsg}`);
965 throw new BaseError(logMsg
);
967 if (Utils
.isEmptyObject(stationInfo
)) {
969 `${this.logPrefix()} Empty charging station information from template file ${
974 const chargingStationId
= this.getChargingStationId(stationInfo
);
975 // Deprecation template keys section
976 this.warnDeprecatedTemplateKey(
980 "Use 'supervisionUrls' instead"
982 this.convertDeprecatedTemplateKey(stationInfo
, 'supervisionUrl', 'supervisionUrls');
983 stationInfo
.wsOptions
= stationInfo
?.wsOptions
?? {};
984 if (!Utils
.isEmptyArray(stationInfo
.power
)) {
985 stationInfo
.power
= stationInfo
.power
as number[];
986 const powerArrayRandomIndex
= Math.floor(Utils
.secureRandom() * stationInfo
.power
.length
);
987 stationInfo
.maximumPower
=
988 stationInfo
.powerUnit
=== PowerUnits
.KILO_WATT
989 ? stationInfo
.power
[powerArrayRandomIndex
] * 1000
990 : stationInfo
.power
[powerArrayRandomIndex
];
992 stationInfo
.power
= stationInfo
.power
as number;
993 stationInfo
.maximumPower
=
994 stationInfo
.powerUnit
=== PowerUnits
.KILO_WATT
995 ? stationInfo
.power
* 1000
998 delete stationInfo
.power
;
999 delete stationInfo
.powerUnit
;
1000 stationInfo
.chargingStationId
= chargingStationId
;
1001 stationInfo
.resetTime
= stationInfo
.resetTime
1002 ? stationInfo
.resetTime
* 1000
1003 : Constants
.CHARGING_STATION_DEFAULT_RESET_TIME
;
1007 private createStationInfoHash(stationInfo
: ChargingStationInfo
): ChargingStationInfo
{
1008 if (!Utils
.isEmptyObject(stationInfo
)) {
1009 const previousInfoHash
= stationInfo
?.infoHash
?? '';
1010 delete stationInfo
.infoHash
;
1011 const currentInfoHash
= crypto
1012 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1013 .update(JSON
.stringify(stationInfo
))
1016 Utils
.isEmptyString(previousInfoHash
) ||
1017 (!Utils
.isEmptyString(previousInfoHash
) && currentInfoHash
!== previousInfoHash
)
1019 stationInfo
.infoHash
= currentInfoHash
;
1021 stationInfo
.infoHash
= previousInfoHash
;
1027 private getStationInfoFromFile(): ChargingStationInfo
{
1028 let stationInfo
= this.getConfigurationFromFile()?.stationInfo
?? ({} as ChargingStationInfo
);
1029 stationInfo
= this.createStationInfoHash(stationInfo
);
1033 private getStationInfo(): ChargingStationInfo
{
1034 const stationInfoFromTemplate
: ChargingStationInfo
= this.getStationInfoFromTemplate();
1035 this.hashId
= this.getHashId(stationInfoFromTemplate
);
1036 this.configurationFile
= path
.join(
1037 path
.resolve(__dirname
, '../'),
1040 this.hashId
+ '.json'
1042 const stationInfoFromFile
: ChargingStationInfo
= this.getStationInfoFromFile();
1043 // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
1044 if (stationInfoFromFile
?.templateHash
=== stationInfoFromTemplate
.templateHash
) {
1045 if (this.stationInfo
?.infoHash
=== stationInfoFromFile
?.infoHash
) {
1046 return this.stationInfo
;
1048 return stationInfoFromFile
;
1050 this.createSerialNumber(stationInfoFromTemplate
, stationInfoFromFile
);
1051 return stationInfoFromTemplate
;
1054 private saveStationInfo(): void {
1055 this.saveConfiguration(Section
.stationInfo
);
1058 private getOcppVersion(): OCPPVersion
{
1059 return this.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
1062 private getOcppPersistentConfiguration(): boolean {
1063 return this.stationInfo
.ocppPersistentConfiguration
?? true;
1066 private handleUnsupportedVersion(version
: OCPPVersion
) {
1067 const errMsg
= `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${
1070 logger
.error(errMsg
);
1071 throw new Error(errMsg
);
1074 private createBootNotificationRequest(stationInfo
: ChargingStationInfo
): BootNotificationRequest
{
1076 chargePointModel
: stationInfo
.chargePointModel
,
1077 chargePointVendor
: stationInfo
.chargePointVendor
,
1078 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
1079 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
1081 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumber
) && {
1082 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
1084 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
1085 firmwareVersion
: stationInfo
.firmwareVersion
,
1087 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
1088 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
1089 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumber
) && {
1090 meterSerialNumber
: stationInfo
.meterSerialNumber
,
1092 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
1093 meterType
: stationInfo
.meterType
,
1098 private getHashId(stationInfo
: ChargingStationInfo
): string {
1099 const hashBootNotificationRequest
= {
1100 chargePointModel
: stationInfo
.chargePointModel
,
1101 chargePointVendor
: stationInfo
.chargePointVendor
,
1102 ...(!Utils
.isUndefined(stationInfo
.chargeBoxSerialNumberPrefix
) && {
1103 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumberPrefix
,
1105 ...(!Utils
.isUndefined(stationInfo
.chargePointSerialNumberPrefix
) && {
1106 chargePointSerialNumber
: stationInfo
.chargePointSerialNumberPrefix
,
1108 ...(!Utils
.isUndefined(stationInfo
.firmwareVersion
) && {
1109 firmwareVersion
: stationInfo
.firmwareVersion
,
1111 ...(!Utils
.isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
1112 ...(!Utils
.isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
1113 ...(!Utils
.isUndefined(stationInfo
.meterSerialNumberPrefix
) && {
1114 meterSerialNumber
: stationInfo
.meterSerialNumberPrefix
,
1116 ...(!Utils
.isUndefined(stationInfo
.meterType
) && {
1117 meterType
: stationInfo
.meterType
,
1121 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1122 .update(JSON
.stringify(hashBootNotificationRequest
) + stationInfo
.chargingStationId
)
1126 private initialize(): void {
1127 this.stationInfo
= this.getStationInfo();
1128 logger
.info(`${this.logPrefix()} Charging station hashId '${this.hashId}'`);
1129 this.bootNotificationRequest
= this.createBootNotificationRequest(this.stationInfo
);
1130 this.ocppConfiguration
= this.getOcppConfiguration();
1131 this.stationInfo
?.Configuration
&& delete this.stationInfo
.Configuration
;
1132 this.wsConfiguredConnectionUrl
= new URL(
1133 this.getConfiguredSupervisionUrl().href
+ '/' + this.stationInfo
.chargingStationId
1135 // Build connectors if needed
1136 const maxConnectors
= this.getMaxNumberOfConnectors();
1137 this.checkMaxConnectors(maxConnectors
);
1138 const templateMaxConnectors
= this.getTemplateMaxNumberOfConnectors();
1139 this.checkTemplateMaxConnectors(templateMaxConnectors
);
1142 (this.stationInfo
?.Connectors
[0] ? templateMaxConnectors
- 1 : templateMaxConnectors
) &&
1143 !this.stationInfo
.randomConnectors
1146 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
1148 }, forcing random connector configurations affectation`
1150 this.stationInfo
.randomConnectors
= true;
1152 this.initializeConnectors(this.stationInfo
, maxConnectors
, templateMaxConnectors
);
1153 this.stationInfo
.maximumAmperage
= this.getMaximumAmperage();
1154 this.stationInfo
= this.createStationInfoHash(this.stationInfo
);
1155 this.saveStationInfo();
1156 // Avoid duplication of connectors related information in RAM
1157 this.stationInfo
?.Connectors
&& delete this.stationInfo
.Connectors
;
1158 // OCPP configuration
1159 this.initializeOcppConfiguration();
1160 if (this.getEnableStatistics()) {
1161 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1163 this.stationInfo
.chargingStationId
,
1164 this.wsConnectionUrl
1167 switch (this.getOcppVersion()) {
1168 case OCPPVersion
.VERSION_16
:
1169 this.ocppIncomingRequestService
=
1170 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>(this);
1171 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1173 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>(this)
1177 this.handleUnsupportedVersion(this.getOcppVersion());
1180 if (this.stationInfo
.autoRegister
) {
1181 this.bootNotificationResponse
= {
1182 currentTime
: new Date().toISOString(),
1183 interval
: this.getHeartbeatInterval() / 1000,
1184 status: RegistrationStatus
.ACCEPTED
,
1187 this.stationInfo
.powerDivider
= this.getPowerDivider();
1190 private initializeOcppConfiguration(): void {
1191 if (!this.getConfigurationKey(StandardParametersKey
.HeartbeatInterval
)) {
1192 this.addConfigurationKey(StandardParametersKey
.HeartbeatInterval
, '0');
1194 if (!this.getConfigurationKey(StandardParametersKey
.HeartBeatInterval
)) {
1195 this.addConfigurationKey(StandardParametersKey
.HeartBeatInterval
, '0', { visible
: false });
1198 this.getSupervisionUrlOcppConfiguration() &&
1199 !this.getConfigurationKey(this.getSupervisionUrlOcppKey())
1201 this.addConfigurationKey(
1202 this.getSupervisionUrlOcppKey(),
1203 this.getConfiguredSupervisionUrl().href
,
1207 !this.getSupervisionUrlOcppConfiguration() &&
1208 this.getConfigurationKey(this.getSupervisionUrlOcppKey())
1210 this.deleteConfigurationKey(this.getSupervisionUrlOcppKey(), { save
: false });
1213 this.stationInfo
.amperageLimitationOcppKey
&&
1214 !this.getConfigurationKey(this.stationInfo
.amperageLimitationOcppKey
)
1216 this.addConfigurationKey(
1217 this.stationInfo
.amperageLimitationOcppKey
,
1218 (this.stationInfo
.maximumAmperage
* this.getAmperageLimitationUnitDivider()).toString()
1221 if (!this.getConfigurationKey(StandardParametersKey
.SupportedFeatureProfiles
)) {
1222 this.addConfigurationKey(
1223 StandardParametersKey
.SupportedFeatureProfiles
,
1224 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1227 this.addConfigurationKey(
1228 StandardParametersKey
.NumberOfConnectors
,
1229 this.getNumberOfConnectors().toString(),
1233 if (!this.getConfigurationKey(StandardParametersKey
.MeterValuesSampledData
)) {
1234 this.addConfigurationKey(
1235 StandardParametersKey
.MeterValuesSampledData
,
1236 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1239 if (!this.getConfigurationKey(StandardParametersKey
.ConnectorPhaseRotation
)) {
1240 const connectorPhaseRotation
= [];
1241 for (const connectorId
of this.connectors
.keys()) {
1243 if (connectorId
=== 0 && this.getNumberOfPhases() === 0) {
1244 connectorPhaseRotation
.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1245 } else if (connectorId
> 0 && this.getNumberOfPhases() === 0) {
1246 connectorPhaseRotation
.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1248 } else if (connectorId
> 0 && this.getNumberOfPhases() === 1) {
1249 connectorPhaseRotation
.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1250 } else if (connectorId
> 0 && this.getNumberOfPhases() === 3) {
1251 connectorPhaseRotation
.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1254 this.addConfigurationKey(
1255 StandardParametersKey
.ConnectorPhaseRotation
,
1256 connectorPhaseRotation
.toString()
1259 if (!this.getConfigurationKey(StandardParametersKey
.AuthorizeRemoteTxRequests
)) {
1260 this.addConfigurationKey(StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true');
1263 !this.getConfigurationKey(StandardParametersKey
.LocalAuthListEnabled
) &&
1264 this.getConfigurationKey(StandardParametersKey
.SupportedFeatureProfiles
)?.value
.includes(
1265 SupportedFeatureProfiles
.LocalAuthListManagement
1268 this.addConfigurationKey(StandardParametersKey
.LocalAuthListEnabled
, 'false');
1270 if (!this.getConfigurationKey(StandardParametersKey
.ConnectionTimeOut
)) {
1271 this.addConfigurationKey(
1272 StandardParametersKey
.ConnectionTimeOut
,
1273 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1276 this.saveOcppConfiguration();
1279 private initializeConnectors(
1280 stationInfo
: ChargingStationInfo
,
1281 maxConnectors
: number,
1282 templateMaxConnectors
: number
1284 if (!stationInfo
?.Connectors
&& this.connectors
.size
=== 0) {
1285 const logMsg
= `${this.logPrefix()} No already defined connectors and charging station information from template ${
1287 } with no connectors configuration defined`;
1288 logger
.error(logMsg
);
1289 throw new BaseError(logMsg
);
1291 if (!stationInfo
?.Connectors
[0]) {
1293 `${this.logPrefix()} Charging station information from template ${
1295 } with no connector Id 0 configuration`
1298 if (stationInfo
?.Connectors
) {
1299 const connectorsConfigHash
= crypto
1300 .createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1301 .update(JSON
.stringify(stationInfo
?.Connectors
) + maxConnectors
.toString())
1303 const connectorsConfigChanged
=
1304 this.connectors
?.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
;
1305 if (this.connectors
?.size
=== 0 || connectorsConfigChanged
) {
1306 connectorsConfigChanged
&& this.connectors
.clear();
1307 this.connectorsConfigurationHash
= connectorsConfigHash
;
1308 // Add connector Id 0
1309 let lastConnector
= '0';
1310 for (lastConnector
in stationInfo
?.Connectors
) {
1311 const lastConnectorId
= Utils
.convertToInt(lastConnector
);
1313 lastConnectorId
=== 0 &&
1314 this.getUseConnectorId0() &&
1315 stationInfo
?.Connectors
[lastConnector
]
1317 this.connectors
.set(
1319 Utils
.cloneObject
<ConnectorStatus
>(stationInfo
?.Connectors
[lastConnector
])
1321 this.getConnectorStatus(lastConnectorId
).availability
= AvailabilityType
.OPERATIVE
;
1322 if (Utils
.isUndefined(this.getConnectorStatus(lastConnectorId
)?.chargingProfiles
)) {
1323 this.getConnectorStatus(lastConnectorId
).chargingProfiles
= [];
1327 // Generate all connectors
1328 if ((stationInfo
?.Connectors
[0] ? templateMaxConnectors
- 1 : templateMaxConnectors
) > 0) {
1329 for (let index
= 1; index
<= maxConnectors
; index
++) {
1330 const randConnectorId
= stationInfo
.randomConnectors
1331 ? Utils
.getRandomInteger(Utils
.convertToInt(lastConnector
), 1)
1333 this.connectors
.set(
1335 Utils
.cloneObject
<ConnectorStatus
>(stationInfo
?.Connectors
[randConnectorId
])
1337 this.getConnectorStatus(index
).availability
= AvailabilityType
.OPERATIVE
;
1338 if (Utils
.isUndefined(this.getConnectorStatus(index
)?.chargingProfiles
)) {
1339 this.getConnectorStatus(index
).chargingProfiles
= [];
1346 `${this.logPrefix()} Charging station information from template ${
1348 } with no connectors configuration defined, using already defined connectors`
1351 // Initialize transaction attributes on connectors
1352 for (const connectorId
of this.connectors
.keys()) {
1353 if (connectorId
> 0 && !this.getConnectorStatus(connectorId
)?.transactionStarted
) {
1354 this.initializeConnectorStatus(connectorId
);
1359 private checkMaxConnectors(maxConnectors
: number): void {
1360 if (maxConnectors
<= 0) {
1362 `${this.logPrefix()} Charging station information from template ${
1364 } with ${maxConnectors} connectors`
1369 private checkTemplateMaxConnectors(templateMaxConnectors
: number): void {
1370 if (templateMaxConnectors
=== 0) {
1372 `${this.logPrefix()} Charging station information from template ${
1374 } with empty connectors configuration`
1376 } else if (templateMaxConnectors
< 0) {
1378 `${this.logPrefix()} Charging station information from template ${
1380 } with no connectors configuration defined`
1385 private getConfigurationFromFile(): ChargingStationConfiguration
| null {
1386 let configuration
: ChargingStationConfiguration
= null;
1387 if (this.configurationFile
&& fs
.existsSync(this.configurationFile
)) {
1389 const measureId
= `${FileType.ChargingStationConfiguration} read`;
1390 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1391 configuration
= JSON
.parse(
1392 fs
.readFileSync(this.configurationFile
, 'utf8')
1393 ) as ChargingStationConfiguration
;
1394 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1396 FileUtils
.handleFileException(
1398 FileType
.ChargingStationConfiguration
,
1399 this.configurationFile
,
1400 error
as NodeJS
.ErrnoException
1404 return configuration
;
1407 private saveConfiguration(section
?: Section
): void {
1408 if (this.configurationFile
) {
1410 const configurationData
: ChargingStationConfiguration
=
1411 this.getConfigurationFromFile() ?? {};
1412 if (!fs
.existsSync(path
.dirname(this.configurationFile
))) {
1413 fs
.mkdirSync(path
.dirname(this.configurationFile
), { recursive
: true });
1416 case Section
.ocppConfiguration
:
1417 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
;
1419 case Section
.stationInfo
:
1420 if (configurationData
?.stationInfo
?.infoHash
=== this.stationInfo
?.infoHash
) {
1422 `${this.logPrefix()} Not saving unchanged charging station information to configuration file ${
1423 this.configurationFile
1428 configurationData
.stationInfo
= this.stationInfo
;
1431 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
;
1432 if (configurationData
?.stationInfo
?.infoHash
!== this.stationInfo
?.infoHash
) {
1433 configurationData
.stationInfo
= this.stationInfo
;
1437 const measureId
= `${FileType.ChargingStationConfiguration} write`;
1438 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1439 const fileDescriptor
= fs
.openSync(this.configurationFile
, 'w');
1440 fs
.writeFileSync(fileDescriptor
, JSON
.stringify(configurationData
, null, 2), 'utf8');
1441 fs
.closeSync(fileDescriptor
);
1442 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1444 FileUtils
.handleFileException(
1446 FileType
.ChargingStationConfiguration
,
1447 this.configurationFile
,
1448 error
as NodeJS
.ErrnoException
1453 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1458 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration
{
1459 return this.getTemplateFromFile().Configuration
?? ({} as ChargingStationOcppConfiguration
);
1462 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration
| null {
1463 let configuration
: ChargingStationConfiguration
= null;
1464 if (this.getOcppPersistentConfiguration()) {
1465 const configurationFromFile
= this.getConfigurationFromFile();
1466 configuration
= configurationFromFile
?.configurationKey
&& configurationFromFile
;
1468 configuration
&& delete configuration
.stationInfo
;
1469 return configuration
;
1472 private getOcppConfiguration(): ChargingStationOcppConfiguration
{
1473 let ocppConfiguration
: ChargingStationOcppConfiguration
= this.getOcppConfigurationFromFile();
1474 if (!ocppConfiguration
) {
1475 ocppConfiguration
= this.getOcppConfigurationFromTemplate();
1477 return ocppConfiguration
;
1480 private saveOcppConfiguration(): void {
1481 if (this.getOcppPersistentConfiguration()) {
1482 this.saveConfiguration(Section
.ocppConfiguration
);
1486 private async onOpen(): Promise
<void> {
1487 if (this.isWebSocketConnectionOpened()) {
1489 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1491 if (!this.isRegistered()) {
1492 // Send BootNotification
1493 let registrationRetryCount
= 0;
1495 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1496 BootNotificationRequest
,
1497 BootNotificationResponse
1499 RequestCommand
.BOOT_NOTIFICATION
,
1501 chargePointModel
: this.bootNotificationRequest
.chargePointModel
,
1502 chargePointVendor
: this.bootNotificationRequest
.chargePointVendor
,
1503 chargeBoxSerialNumber
: this.bootNotificationRequest
.chargeBoxSerialNumber
,
1504 firmwareVersion
: this.bootNotificationRequest
.firmwareVersion
,
1505 chargePointSerialNumber
: this.bootNotificationRequest
.chargePointSerialNumber
,
1506 iccid
: this.bootNotificationRequest
.iccid
,
1507 imsi
: this.bootNotificationRequest
.imsi
,
1508 meterSerialNumber
: this.bootNotificationRequest
.meterSerialNumber
,
1509 meterType
: this.bootNotificationRequest
.meterType
,
1511 { skipBufferingOnError
: true }
1513 if (!this.isRegistered()) {
1514 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount
++;
1516 this.bootNotificationResponse
?.interval
1517 ? this.bootNotificationResponse
.interval
* 1000
1518 : Constants
.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1522 !this.isRegistered() &&
1523 (registrationRetryCount
<= this.getRegistrationMaxRetries() ||
1524 this.getRegistrationMaxRetries() === -1)
1527 if (this.isRegistered()) {
1528 if (this.isInAcceptedState()) {
1529 await this.startMessageSequence();
1530 this.wsConnectionRestarted
&& this.flushMessageBuffer();
1534 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1537 this.stopped
&& (this.stopped
= false);
1538 this.autoReconnectRetryCount
= 0;
1539 this.wsConnectionRestarted
= false;
1542 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1547 private async onClose(code
: number, reason
: string): Promise
<void> {
1550 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1551 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1553 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1555 )}' and reason '${reason}'`
1557 this.autoReconnectRetryCount
= 0;
1562 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1564 )}' and reason '${reason}'`
1566 await this.reconnect(code
);
1571 private async onMessage(data
: Data
): Promise
<void> {
1572 let messageType
: number;
1573 let messageId
: string;
1574 let commandName
: IncomingRequestCommand
;
1575 let commandPayload
: JsonType
;
1576 let errorType
: ErrorType
;
1577 let errorMessage
: string;
1578 let errorDetails
: JsonType
;
1579 let responseCallback
: (payload
: JsonType
, requestPayload
: JsonType
) => void;
1580 let errorCallback
: (error
: OCPPError
, requestStatistic
?: boolean) => void;
1581 let requestCommandName
: RequestCommand
| IncomingRequestCommand
;
1582 let requestPayload
: JsonType
;
1583 let cachedRequest
: CachedRequest
;
1586 const request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
;
1587 if (Utils
.isIterable(request
)) {
1588 [messageType
, messageId
] = request
;
1589 // Check the type of message
1590 switch (messageType
) {
1592 case MessageType
.CALL_MESSAGE
:
1593 [, , commandName
, commandPayload
] = request
as IncomingRequest
;
1594 if (this.getEnableStatistics()) {
1595 this.performanceStatistics
.addRequestStatistic(commandName
, messageType
);
1598 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1602 // Process the message
1603 await this.ocppIncomingRequestService
.incomingRequestHandler(
1610 case MessageType
.CALL_RESULT_MESSAGE
:
1611 [, , commandPayload
] = request
as Response
;
1612 if (!this.requests
.has(messageId
)) {
1614 throw new OCPPError(
1615 ErrorType
.INTERNAL_ERROR
,
1616 `Response for unknown message id ${messageId}`,
1622 cachedRequest
= this.requests
.get(messageId
);
1623 if (Utils
.isIterable(cachedRequest
)) {
1624 [responseCallback
, , requestCommandName
, requestPayload
] = cachedRequest
;
1626 throw new OCPPError(
1627 ErrorType
.PROTOCOL_ERROR
,
1628 `Cached request for message id ${messageId} response is not iterable`,
1630 cachedRequest
as unknown
as JsonType
1634 `${this.logPrefix()} << Command '${
1635 requestCommandName ?? ''
1636 }' received response payload: ${JSON.stringify(request)}`
1638 responseCallback(commandPayload
, requestPayload
);
1641 case MessageType
.CALL_ERROR_MESSAGE
:
1642 [, , errorType
, errorMessage
, errorDetails
] = request
as ErrorResponse
;
1643 if (!this.requests
.has(messageId
)) {
1645 throw new OCPPError(
1646 ErrorType
.INTERNAL_ERROR
,
1647 `Error response for unknown message id ${messageId}`,
1649 { errorType
, errorMessage
, errorDetails
}
1652 cachedRequest
= this.requests
.get(messageId
);
1653 if (Utils
.isIterable(cachedRequest
)) {
1654 [, errorCallback
, requestCommandName
] = cachedRequest
;
1656 throw new OCPPError(
1657 ErrorType
.PROTOCOL_ERROR
,
1658 `Cached request for message id ${messageId} error response is not iterable`,
1660 cachedRequest
as unknown
as JsonType
1664 `${this.logPrefix()} << Command '${
1665 requestCommandName ?? ''
1666 }' received error payload: ${JSON.stringify(request)}`
1668 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
));
1672 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1673 errMsg
= `${this.logPrefix()} Wrong message type ${messageType}`;
1674 logger
.error(errMsg
);
1675 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errMsg
);
1678 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, 'Incoming message is not iterable', null, {
1685 '%s Incoming OCPP message %j matching cached request %j processing error %j',
1688 this.requests
.get(messageId
),
1692 messageType
=== MessageType
.CALL_MESSAGE
&&
1693 (await this.ocppRequestService
.sendError(
1696 commandName
?? requestCommandName
?? null
1701 private onPing(): void {
1702 logger
.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
1705 private onPong(): void {
1706 logger
.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
1709 private onError(error
: WSError
): void {
1710 logger
.error(this.logPrefix() + ' WebSocket error: %j', error
);
1713 private getAuthorizationFile(): string | undefined {
1715 this.stationInfo
.authorizationFile
&&
1717 path
.resolve(__dirname
, '../'),
1719 path
.basename(this.stationInfo
.authorizationFile
)
1724 private getAuthorizedTags(): string[] {
1725 let authorizedTags
: string[] = [];
1726 const authorizationFile
= this.getAuthorizationFile();
1727 if (authorizationFile
) {
1729 // Load authorization file
1730 authorizedTags
= JSON
.parse(fs
.readFileSync(authorizationFile
, 'utf8')) as string[];
1732 FileUtils
.handleFileException(
1734 FileType
.Authorization
,
1736 error
as NodeJS
.ErrnoException
1741 this.logPrefix() + ' No authorization file given in template file ' + this.templateFile
1744 return authorizedTags
;
1747 private getUseConnectorId0(): boolean | undefined {
1748 return !Utils
.isUndefined(this.stationInfo
.useConnectorId0
)
1749 ? this.stationInfo
.useConnectorId0
1753 private getNumberOfRunningTransactions(): number {
1755 for (const connectorId
of this.connectors
.keys()) {
1756 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
) {
1764 private getConnectionTimeout(): number | undefined {
1765 if (this.getConfigurationKey(StandardParametersKey
.ConnectionTimeOut
)) {
1767 parseInt(this.getConfigurationKey(StandardParametersKey
.ConnectionTimeOut
).value
) ??
1768 Constants
.DEFAULT_CONNECTION_TIMEOUT
1771 return Constants
.DEFAULT_CONNECTION_TIMEOUT
;
1774 // -1 for unlimited, 0 for disabling
1775 private getAutoReconnectMaxRetries(): number | undefined {
1776 if (!Utils
.isUndefined(this.stationInfo
.autoReconnectMaxRetries
)) {
1777 return this.stationInfo
.autoReconnectMaxRetries
;
1779 if (!Utils
.isUndefined(Configuration
.getAutoReconnectMaxRetries())) {
1780 return Configuration
.getAutoReconnectMaxRetries();
1786 private getRegistrationMaxRetries(): number | undefined {
1787 if (!Utils
.isUndefined(this.stationInfo
.registrationMaxRetries
)) {
1788 return this.stationInfo
.registrationMaxRetries
;
1793 private getPowerDivider(): number {
1794 let powerDivider
= this.getNumberOfConnectors();
1795 if (this.stationInfo
.powerSharedByConnectors
) {
1796 powerDivider
= this.getNumberOfRunningTransactions();
1798 return powerDivider
;
1801 private getTemplateMaxNumberOfConnectors(): number {
1802 if (!this.stationInfo
?.Connectors
) {
1805 return Object.keys(this.stationInfo
?.Connectors
).length
;
1808 private getMaxNumberOfConnectors(): number {
1809 let maxConnectors
: number;
1810 if (!Utils
.isEmptyArray(this.stationInfo
.numberOfConnectors
)) {
1811 const numberOfConnectors
= this.stationInfo
.numberOfConnectors
as number[];
1812 // Distribute evenly the number of connectors
1813 maxConnectors
= numberOfConnectors
[(this.index
- 1) % numberOfConnectors
.length
];
1814 } else if (!Utils
.isUndefined(this.stationInfo
.numberOfConnectors
)) {
1815 maxConnectors
= this.stationInfo
.numberOfConnectors
as number;
1817 maxConnectors
= this.stationInfo
?.Connectors
[0]
1818 ? this.getTemplateMaxNumberOfConnectors() - 1
1819 : this.getTemplateMaxNumberOfConnectors();
1821 return maxConnectors
;
1824 private getMaximumPower(): number {
1825 return (this.stationInfo
['maxPower'] as number) ?? this.stationInfo
.maximumPower
;
1828 private getMaximumAmperage(): number | undefined {
1829 const maximumPower
= this.getMaximumPower();
1830 switch (this.getCurrentOutType()) {
1831 case CurrentType
.AC
:
1832 return ACElectricUtils
.amperagePerPhaseFromPower(
1833 this.getNumberOfPhases(),
1834 maximumPower
/ this.getNumberOfConnectors(),
1835 this.getVoltageOut()
1837 case CurrentType
.DC
:
1838 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut());
1842 private getAmperageLimitationUnitDivider(): number {
1843 let unitDivider
= 1;
1844 switch (this.stationInfo
.amperageLimitationUnit
) {
1845 case AmpereUnits
.DECI_AMPERE
:
1848 case AmpereUnits
.CENTI_AMPERE
:
1851 case AmpereUnits
.MILLI_AMPERE
:
1858 private getAmperageLimitation(): number | undefined {
1860 this.stationInfo
.amperageLimitationOcppKey
&&
1861 this.getConfigurationKey(this.stationInfo
.amperageLimitationOcppKey
)
1865 this.getConfigurationKey(this.stationInfo
.amperageLimitationOcppKey
).value
1866 ) / this.getAmperageLimitationUnitDivider()
1871 private async startMessageSequence(): Promise
<void> {
1872 if (this.stationInfo
.autoRegister
) {
1873 await this.ocppRequestService
.requestHandler
<
1874 BootNotificationRequest
,
1875 BootNotificationResponse
1877 RequestCommand
.BOOT_NOTIFICATION
,
1879 chargePointModel
: this.bootNotificationRequest
.chargePointModel
,
1880 chargePointVendor
: this.bootNotificationRequest
.chargePointVendor
,
1881 chargeBoxSerialNumber
: this.bootNotificationRequest
.chargeBoxSerialNumber
,
1882 firmwareVersion
: this.bootNotificationRequest
.firmwareVersion
,
1883 chargePointSerialNumber
: this.bootNotificationRequest
.chargePointSerialNumber
,
1884 iccid
: this.bootNotificationRequest
.iccid
,
1885 imsi
: this.bootNotificationRequest
.imsi
,
1886 meterSerialNumber
: this.bootNotificationRequest
.meterSerialNumber
,
1887 meterType
: this.bootNotificationRequest
.meterType
,
1889 { skipBufferingOnError
: true }
1892 // Start WebSocket ping
1893 this.startWebSocketPing();
1895 this.startHeartbeat();
1896 // Initialize connectors status
1897 for (const connectorId
of this.connectors
.keys()) {
1898 if (connectorId
=== 0) {
1902 !this.getConnectorStatus(connectorId
)?.status &&
1903 this.getConnectorStatus(connectorId
)?.bootStatus
1905 // Send status in template at startup
1906 await this.ocppRequestService
.requestHandler
<
1907 StatusNotificationRequest
,
1908 StatusNotificationResponse
1909 >(RequestCommand
.STATUS_NOTIFICATION
, {
1911 status: this.getConnectorStatus(connectorId
).bootStatus
,
1912 errorCode
: ChargePointErrorCode
.NO_ERROR
,
1914 this.getConnectorStatus(connectorId
).status =
1915 this.getConnectorStatus(connectorId
).bootStatus
;
1918 this.getConnectorStatus(connectorId
)?.status &&
1919 this.getConnectorStatus(connectorId
)?.bootStatus
1921 // Send status in template after reset
1922 await this.ocppRequestService
.requestHandler
<
1923 StatusNotificationRequest
,
1924 StatusNotificationResponse
1925 >(RequestCommand
.STATUS_NOTIFICATION
, {
1927 status: this.getConnectorStatus(connectorId
).bootStatus
,
1928 errorCode
: ChargePointErrorCode
.NO_ERROR
,
1930 this.getConnectorStatus(connectorId
).status =
1931 this.getConnectorStatus(connectorId
).bootStatus
;
1932 } else if (!this.stopped
&& this.getConnectorStatus(connectorId
)?.status) {
1933 // Send previous status at template reload
1934 await this.ocppRequestService
.requestHandler
<
1935 StatusNotificationRequest
,
1936 StatusNotificationResponse
1937 >(RequestCommand
.STATUS_NOTIFICATION
, {
1939 status: this.getConnectorStatus(connectorId
).status,
1940 errorCode
: ChargePointErrorCode
.NO_ERROR
,
1943 // Send default status
1944 await this.ocppRequestService
.requestHandler
<
1945 StatusNotificationRequest
,
1946 StatusNotificationResponse
1947 >(RequestCommand
.STATUS_NOTIFICATION
, {
1949 status: ChargePointStatus
.AVAILABLE
,
1950 errorCode
: ChargePointErrorCode
.NO_ERROR
,
1952 this.getConnectorStatus(connectorId
).status = ChargePointStatus
.AVAILABLE
;
1956 this.startAutomaticTransactionGenerator();
1959 private startAutomaticTransactionGenerator() {
1960 if (this.stationInfo
.AutomaticTransactionGenerator
.enable
) {
1961 if (!this.automaticTransactionGenerator
) {
1962 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this);
1964 if (!this.automaticTransactionGenerator
.started
) {
1965 this.automaticTransactionGenerator
.start();
1970 private async stopMessageSequence(
1971 reason
: StopTransactionReason
= StopTransactionReason
.NONE
1973 // Stop WebSocket ping
1974 this.stopWebSocketPing();
1976 this.stopHeartbeat();
1979 this.stationInfo
.AutomaticTransactionGenerator
.enable
&&
1980 this.automaticTransactionGenerator
?.started
1982 this.automaticTransactionGenerator
.stop();
1984 for (const connectorId
of this.connectors
.keys()) {
1985 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
) {
1986 const transactionId
= this.getConnectorStatus(connectorId
).transactionId
;
1988 this.getBeginEndMeterValues() &&
1989 this.getOcppStrictCompliance() &&
1990 !this.getOutOfOrderEndMeterValues()
1992 // FIXME: Implement OCPP version agnostic helpers
1993 const transactionEndMeterValue
= OCPP16ServiceUtils
.buildTransactionEndMeterValue(
1996 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
1998 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
1999 RequestCommand
.METER_VALUES
,
2003 meterValue
: transactionEndMeterValue
,
2007 await this.ocppRequestService
.requestHandler
<
2008 StopTransactionRequest
,
2009 StopTransactionResponse
2010 >(RequestCommand
.STOP_TRANSACTION
, {
2012 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
),
2013 idTag
: this.getTransactionIdTag(transactionId
),
2021 private startWebSocketPing(): void {
2022 const webSocketPingInterval
: number = this.getConfigurationKey(
2023 StandardParametersKey
.WebSocketPingInterval
2025 ? Utils
.convertToInt(
2026 this.getConfigurationKey(StandardParametersKey
.WebSocketPingInterval
).value
2029 if (webSocketPingInterval
> 0 && !this.webSocketPingSetInterval
) {
2030 this.webSocketPingSetInterval
= setInterval(() => {
2031 if (this.isWebSocketConnectionOpened()) {
2032 this.wsConnection
.ping((): void => {
2033 /* This is intentional */
2036 }, webSocketPingInterval
* 1000);
2039 ' WebSocket ping started every ' +
2040 Utils
.formatDurationSeconds(webSocketPingInterval
)
2042 } else if (this.webSocketPingSetInterval
) {
2045 ' WebSocket ping every ' +
2046 Utils
.formatDurationSeconds(webSocketPingInterval
) +
2051 `${this.logPrefix()} WebSocket ping interval set to ${
2052 webSocketPingInterval
2053 ? Utils.formatDurationSeconds(webSocketPingInterval)
2054 : webSocketPingInterval
2055 }, not starting the WebSocket ping`
2060 private stopWebSocketPing(): void {
2061 if (this.webSocketPingSetInterval
) {
2062 clearInterval(this.webSocketPingSetInterval
);
2066 private warnDeprecatedTemplateKey(
2067 template
: ChargingStationTemplate
,
2069 chargingStationId
: string,
2072 if (!Utils
.isUndefined(template
[key
])) {
2073 const logPrefixStr
= ` ${chargingStationId} |`;
2075 `${Utils.logPrefix(logPrefixStr)} Deprecated template key '${key}' usage in file '${
2077 }'${logMsgToAppend && '. ' + logMsgToAppend}`
2082 private convertDeprecatedTemplateKey(
2083 template
: ChargingStationTemplate
,
2084 deprecatedKey
: string,
2087 if (!Utils
.isUndefined(template
[deprecatedKey
])) {
2088 template
[key
] = template
[deprecatedKey
] as unknown
;
2089 delete template
[deprecatedKey
];
2093 private getConfiguredSupervisionUrl(): URL
{
2094 const supervisionUrls
= Utils
.cloneObject
<string | string[]>(
2095 this.stationInfo
.supervisionUrls
?? Configuration
.getSupervisionUrls()
2097 if (!Utils
.isEmptyArray(supervisionUrls
)) {
2099 switch (Configuration
.getSupervisionUrlDistribution()) {
2100 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2101 urlIndex
= (this.index
- 1) % supervisionUrls
.length
;
2103 case SupervisionUrlDistribution
.RANDOM
:
2105 urlIndex
= Math.floor(Utils
.secureRandom() * supervisionUrls
.length
);
2107 case SupervisionUrlDistribution
.SEQUENTIAL
:
2108 if (this.index
<= supervisionUrls
.length
) {
2109 urlIndex
= this.index
- 1;
2112 `${this.logPrefix()} No more configured supervision urls available, using the first one`
2118 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2119 SupervisionUrlDistribution.ROUND_ROBIN
2122 urlIndex
= (this.index
- 1) % supervisionUrls
.length
;
2125 return new URL(supervisionUrls
[urlIndex
]);
2127 return new URL(supervisionUrls
as string);
2130 private getHeartbeatInterval(): number | undefined {
2131 const HeartbeatInterval
= this.getConfigurationKey(StandardParametersKey
.HeartbeatInterval
);
2132 if (HeartbeatInterval
) {
2133 return Utils
.convertToInt(HeartbeatInterval
.value
) * 1000;
2135 const HeartBeatInterval
= this.getConfigurationKey(StandardParametersKey
.HeartBeatInterval
);
2136 if (HeartBeatInterval
) {
2137 return Utils
.convertToInt(HeartBeatInterval
.value
) * 1000;
2139 !this.stationInfo
.autoRegister
&&
2141 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
2142 Constants.DEFAULT_HEARTBEAT_INTERVAL
2145 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
;
2148 private stopHeartbeat(): void {
2149 if (this.heartbeatSetInterval
) {
2150 clearInterval(this.heartbeatSetInterval
);
2154 private openWSConnection(
2155 options
: WsOptions
= this.stationInfo
.wsOptions
,
2156 forceCloseOpened
= false
2158 options
.handshakeTimeout
= options
?.handshakeTimeout
?? this.getConnectionTimeout() * 1000;
2160 !Utils
.isNullOrUndefined(this.stationInfo
.supervisionUser
) &&
2161 !Utils
.isNullOrUndefined(this.stationInfo
.supervisionPassword
)
2163 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
2165 if (this.isWebSocketConnectionOpened() && forceCloseOpened
) {
2166 this.wsConnection
.close();
2168 let protocol
: string;
2169 switch (this.getOcppVersion()) {
2170 case OCPPVersion
.VERSION_16
:
2171 protocol
= 'ocpp' + OCPPVersion
.VERSION_16
;
2174 this.handleUnsupportedVersion(this.getOcppVersion());
2177 this.wsConnection
= new WebSocket(this.wsConnectionUrl
, protocol
, options
);
2179 this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl
.toString()
2183 private stopMeterValues(connectorId
: number) {
2184 if (this.getConnectorStatus(connectorId
)?.transactionSetInterval
) {
2185 clearInterval(this.getConnectorStatus(connectorId
).transactionSetInterval
);
2189 private getReconnectExponentialDelay(): boolean | undefined {
2190 return !Utils
.isUndefined(this.stationInfo
.reconnectExponentialDelay
)
2191 ? this.stationInfo
.reconnectExponentialDelay
2195 private async reconnect(code
: number): Promise
<void> {
2196 // Stop WebSocket ping
2197 this.stopWebSocketPing();
2199 this.stopHeartbeat();
2200 // Stop the ATG if needed
2202 this.stationInfo
.AutomaticTransactionGenerator
.enable
&&
2203 this.stationInfo
.AutomaticTransactionGenerator
.stopOnConnectionFailure
&&
2204 this.automaticTransactionGenerator
?.started
2206 this.automaticTransactionGenerator
.stop();
2209 this.autoReconnectRetryCount
< this.getAutoReconnectMaxRetries() ||
2210 this.getAutoReconnectMaxRetries() === -1
2212 this.autoReconnectRetryCount
++;
2213 const reconnectDelay
= this.getReconnectExponentialDelay()
2214 ? Utils
.exponentialDelay(this.autoReconnectRetryCount
)
2215 : this.getConnectionTimeout() * 1000;
2216 const reconnectTimeout
= reconnectDelay
- 100 > 0 && reconnectDelay
;
2218 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
2221 )}ms, timeout ${reconnectTimeout}ms`
2223 await Utils
.sleep(reconnectDelay
);
2226 ' WebSocket: reconnecting try #' +
2227 this.autoReconnectRetryCount
.toString()
2229 this.openWSConnection(
2230 { ...this.stationInfo
.wsOptions
, handshakeTimeout
: reconnectTimeout
},
2233 this.wsConnectionRestarted
= true;
2234 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2236 `${this.logPrefix()} WebSocket reconnect failure: maximum retries reached (${
2237 this.autoReconnectRetryCount
2238 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
2243 private initializeConnectorStatus(connectorId
: number): void {
2244 this.getConnectorStatus(connectorId
).idTagLocalAuthorized
= false;
2245 this.getConnectorStatus(connectorId
).idTagAuthorized
= false;
2246 this.getConnectorStatus(connectorId
).transactionRemoteStarted
= false;
2247 this.getConnectorStatus(connectorId
).transactionStarted
= false;
2248 this.getConnectorStatus(connectorId
).energyActiveImportRegisterValue
= 0;
2249 this.getConnectorStatus(connectorId
).transactionEnergyActiveImportRegisterValue
= 0;