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