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