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