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