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