refactor: cleanup isNullOrUndefined usage
[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
66a7748d
JB
3import { createHash } from 'node:crypto'
4import { EventEmitter } from 'node:events'
5import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6import { dirname, join } from 'node:path'
7import { URL } from 'node:url'
8import { parentPort } from 'node:worker_threads'
9
10import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
11import merge from 'just-merge'
12import { type RawData, WebSocket } from 'ws'
13
14import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
15import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
f2d5e3d9
JB
16import {
17 addConfigurationKey,
18 deleteConfigurationKey,
19 getConfigurationKey,
66a7748d
JB
20 setConfigurationKeyValue
21} from './ConfigurationKeyUtils.js'
fba11dc6
JB
22import {
23 buildConnectorsMap,
e6a33233 24 checkChargingStation,
73edcc94 25 checkConfiguration,
fba11dc6
JB
26 checkConnectorsConfiguration,
27 checkStationInfoConnectorStatus,
28 checkTemplate,
fba11dc6
JB
29 createBootNotificationRequest,
30 createSerialNumber,
31 getAmperageLimitationUnitDivider,
32 getBootConnectorStatus,
33 getChargingStationConnectorChargingProfilesPowerLimit,
34 getChargingStationId,
35 getDefaultVoltageOut,
36 getHashId,
37 getIdTagsFile,
38 getMaxNumberOfEvses,
90aceaf6 39 getNumberOfReservableConnectors,
fba11dc6 40 getPhaseRotationValue,
a807045b 41 hasFeatureProfile,
90aceaf6 42 hasReservationExpired,
fba11dc6
JB
43 initializeConnectorsMapStatus,
44 propagateSerialNumber,
45 stationTemplateToStationInfo,
66a7748d
JB
46 warnTemplateKeysDeprecation
47} from './Helpers.js'
48import { IdTagsCache } from './IdTagsCache.js'
08b58f00
JB
49import {
50 OCPP16IncomingRequestService,
51 OCPP16RequestService,
52 OCPP16ResponseService,
08b58f00
JB
53 OCPP20IncomingRequestService,
54 OCPP20RequestService,
55 OCPP20ResponseService,
56 type OCPPIncomingRequestService,
57 type OCPPRequestService,
41f3983a 58 buildMeterValue,
041365be 59 buildStatusNotificationRequest,
41f3983a 60 buildTransactionEndMeterValue,
041365be 61 getMessageTypeString,
66a7748d
JB
62 sendAndSetConnectorStatus
63} from './ocpp/index.js'
64import { SharedLRUCache } from './SharedLRUCache.js'
65import { BaseError, OCPPError } from '../exception/index.js'
66import { PerformanceStatistics } from '../performance/index.js'
e7aeea18 67import {
268a74bb 68 type AutomaticTransactionGeneratorConfiguration,
e7aeea18 69 AvailabilityType,
e0b0ee21 70 type BootNotificationRequest,
268a74bb 71 type BootNotificationResponse,
e0b0ee21 72 type CachedRequest,
268a74bb 73 type ChargingStationConfiguration,
db54d2e0 74 ChargingStationEvents,
268a74bb
JB
75 type ChargingStationInfo,
76 type ChargingStationOcppConfiguration,
77 type ChargingStationTemplate,
c8aafe0d 78 type ConnectorStatus,
268a74bb
JB
79 ConnectorStatusEnum,
80 CurrentType,
e0b0ee21 81 type ErrorCallback,
268a74bb
JB
82 type ErrorResponse,
83 ErrorType,
2585c6e9 84 type EvseStatus,
52952bf8 85 type EvseStatusConfiguration,
268a74bb 86 FileType,
c9a4f9ea
JB
87 FirmwareStatus,
88 type FirmwareStatusNotificationRequest,
268a74bb
JB
89 type FirmwareStatusNotificationResponse,
90 type FirmwareUpgrade,
e0b0ee21 91 type HeartbeatRequest,
268a74bb 92 type HeartbeatResponse,
e0b0ee21 93 type IncomingRequest,
268a74bb 94 type IncomingRequestCommand,
268a74bb 95 MessageType,
268a74bb 96 MeterValueMeasurand,
e0b0ee21 97 type MeterValuesRequest,
268a74bb
JB
98 type MeterValuesResponse,
99 OCPPVersion,
8ca6874c 100 type OutgoingRequest,
268a74bb
JB
101 PowerUnits,
102 RegistrationStatusEnumType,
e7aeea18 103 RequestCommand,
66dd3447 104 type Reservation,
366f75f6 105 type ReservationKey,
66dd3447 106 ReservationTerminationReason,
268a74bb 107 type Response,
268a74bb 108 StandardParametersKey,
5ced7e80 109 type Status,
e0b0ee21 110 type StatusNotificationRequest,
e0b0ee21 111 type StatusNotificationResponse,
66a7748d 112 type StopTransactionReason,
e0b0ee21
JB
113 type StopTransactionRequest,
114 type StopTransactionResponse,
268a74bb
JB
115 SupervisionUrlDistribution,
116 SupportedFeatureProfiles,
66a7748d 117 type Voltage,
268a74bb
JB
118 type WSError,
119 WebSocketCloseEventStatusCode,
66a7748d
JB
120 type WsOptions
121} from '../types/index.js'
60a74391
JB
122import {
123 ACElectricUtils,
1227a6f1
JB
124 AsyncLock,
125 AsyncLockType,
60a74391
JB
126 Configuration,
127 Constants,
128 DCElectricUtils,
179ed367
JB
129 buildChargingStationAutomaticTransactionGeneratorConfiguration,
130 buildConnectorsStatus,
131 buildEvsesStatus,
c8faabc8
JB
132 buildStartedMessage,
133 buildStoppedMessage,
134 buildUpdatedMessage,
9bf0ef23
JB
135 cloneObject,
136 convertToBoolean,
137 convertToInt,
138 exponentialDelay,
139 formatDurationMilliSeconds,
140 formatDurationSeconds,
141 getRandomInteger,
142 getWebSocketCloseEventStatusString,
fa5995d6 143 handleFileException,
9bf0ef23
JB
144 isNotEmptyArray,
145 isNotEmptyString,
9bf0ef23
JB
146 isUndefined,
147 logPrefix,
60a74391 148 logger,
5adf6ca4 149 min,
5f742aac 150 once,
9bf0ef23
JB
151 roundTo,
152 secureRandom,
153 sleep,
66a7748d
JB
154 watchJsonFile
155} from '../utils/index.js'
3f40bc9c 156
db54d2e0 157export class ChargingStation extends EventEmitter {
66a7748d
JB
158 public readonly index: number
159 public readonly templateFile: string
160 public stationInfo!: ChargingStationInfo
161 public started: boolean
162 public starting: boolean
163 public idTagsCache: IdTagsCache
164 public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined
165 public ocppConfiguration!: ChargingStationOcppConfiguration | undefined
166 public wsConnection: WebSocket | null
167 public readonly connectors: Map<number, ConnectorStatus>
168 public readonly evses: Map<number, EvseStatus>
169 public readonly requests: Map<string, CachedRequest>
170 public performanceStatistics!: PerformanceStatistics | undefined
171 public heartbeatSetInterval?: NodeJS.Timeout
172 public ocppRequestService!: OCPPRequestService
173 public bootNotificationRequest!: BootNotificationRequest
174 public bootNotificationResponse!: BootNotificationResponse | undefined
175 public powerDivider!: number
176 private stopping: boolean
177 private configurationFile!: string
178 private configurationFileHash!: string
179 private connectorsConfigurationHash!: string
180 private evsesConfigurationHash!: string
181 private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
182 private ocppIncomingRequestService!: OCPPIncomingRequestService
183 private readonly messageBuffer: Set<string>
184 private configuredSupervisionUrl!: URL
185 private autoReconnectRetryCount: number
186 private templateFileWatcher!: FSWatcher | undefined
187 private templateFileHash!: string
188 private readonly sharedLRUCache: SharedLRUCache
189 private webSocketPingSetInterval?: NodeJS.Timeout
190 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
191 private flushMessageBufferSetInterval?: NodeJS.Timeout
192
193 constructor (index: number, templateFile: string) {
194 super()
195 this.started = false
196 this.starting = false
197 this.stopping = false
198 this.wsConnection = null
199 this.autoReconnectRetryCount = 0
200 this.index = index
201 this.templateFile = templateFile
202 this.connectors = new Map<number, ConnectorStatus>()
203 this.evses = new Map<number, EvseStatus>()
204 this.requests = new Map<string, CachedRequest>()
205 this.messageBuffer = new Set<string>()
206 this.sharedLRUCache = SharedLRUCache.getInstance()
207 this.idTagsCache = IdTagsCache.getInstance()
208 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
32de5a57 209
db54d2e0 210 this.on(ChargingStationEvents.started, () => {
66a7748d
JB
211 parentPort?.postMessage(buildStartedMessage(this))
212 })
db54d2e0 213 this.on(ChargingStationEvents.stopped, () => {
66a7748d
JB
214 parentPort?.postMessage(buildStoppedMessage(this))
215 })
db54d2e0 216 this.on(ChargingStationEvents.updated, () => {
66a7748d
JB
217 parentPort?.postMessage(buildUpdatedMessage(this))
218 })
db54d2e0 219
66a7748d 220 this.initialize()
c0560973
JB
221 }
222
66a7748d
JB
223 public get hasEvses (): boolean {
224 return this.connectors.size === 0 && this.evses.size > 0
a14022a2
JB
225 }
226
66a7748d 227 private get wsConnectionUrl (): URL {
fa7bccf4 228 return new URL(
44eb6026 229 `${
4e3b1d6b 230 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
5398cecf 231 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
66a7748d 232 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5398cecf 233 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)?.value)
66a7748d
JB
234 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
235 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)!.value
44eb6026 236 : this.configuredSupervisionUrl.href
66a7748d
JB
237 }/${this.stationInfo.chargingStationId}`
238 )
12fc74d6
JB
239 }
240
8b7072dc 241 public logPrefix = (): string => {
c1f16afd 242 if (isNotEmptyString(this?.stationInfo?.chargingStationId)) {
66a7748d 243 return logPrefix(` ${this?.stationInfo?.chargingStationId} |`)
c1f16afd 244 }
66a7748d 245 let stationTemplate: ChargingStationTemplate | undefined
c1f16afd
JB
246 try {
247 stationTemplate = JSON.parse(
66a7748d
JB
248 readFileSync(this.templateFile, 'utf8')
249 ) as ChargingStationTemplate
c1f16afd 250 } catch {
66a7748d 251 stationTemplate = undefined
c1f16afd 252 }
66a7748d
JB
253 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
254 }
c0560973 255
66a7748d
JB
256 public hasIdTags (): boolean {
257 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258 return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo)!))
c0560973
JB
259 }
260
66a7748d
JB
261 public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
262 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo
fa7bccf4 263 switch (this.getCurrentOutType(stationInfo)) {
4c2b4904 264 case CurrentType.AC:
66a7748d 265 return localStationInfo.numberOfPhases ?? 3
4c2b4904 266 case CurrentType.DC:
66a7748d 267 return 0
c0560973
JB
268 }
269 }
270
66a7748d
JB
271 public isWebSocketConnectionOpened (): boolean {
272 return this?.wsConnection?.readyState === WebSocket.OPEN
c0560973
JB
273 }
274
66a7748d 275 public inUnknownState (): boolean {
aa63c9b7 276 return this?.bootNotificationResponse?.status == null
73c4266d
JB
277 }
278
66a7748d
JB
279 public inPendingState (): boolean {
280 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
16cd35ad
JB
281 }
282
66a7748d
JB
283 public inAcceptedState (): boolean {
284 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
c0560973
JB
285 }
286
66a7748d
JB
287 public inRejectedState (): boolean {
288 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
16cd35ad
JB
289 }
290
66a7748d
JB
291 public isRegistered (): boolean {
292 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
16cd35ad
JB
293 }
294
66a7748d
JB
295 public isChargingStationAvailable (): boolean {
296 return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
c0560973
JB
297 }
298
66a7748d 299 public hasConnector (connectorId: number): boolean {
a14022a2
JB
300 if (this.hasEvses) {
301 for (const evseStatus of this.evses.values()) {
302 if (evseStatus.connectors.has(connectorId)) {
66a7748d 303 return true
a14022a2
JB
304 }
305 }
66a7748d 306 return false
a14022a2 307 }
66a7748d 308 return this.connectors.has(connectorId)
a14022a2
JB
309 }
310
66a7748d 311 public isConnectorAvailable (connectorId: number): boolean {
28e78158
JB
312 return (
313 connectorId > 0 &&
314 this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
66a7748d 315 )
c0560973
JB
316 }
317
66a7748d 318 public getNumberOfConnectors (): number {
28e78158 319 if (this.hasEvses) {
66a7748d 320 let numberOfConnectors = 0
28e78158 321 for (const [evseId, evseStatus] of this.evses) {
4334db72 322 if (evseId > 0) {
66a7748d 323 numberOfConnectors += evseStatus.connectors.size
28e78158 324 }
28e78158 325 }
66a7748d 326 return numberOfConnectors
28e78158 327 }
66a7748d 328 return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size
54544ef1
JB
329 }
330
66a7748d
JB
331 public getNumberOfEvses (): number {
332 return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
28e78158
JB
333 }
334
66a7748d 335 public getConnectorStatus (connectorId: number): ConnectorStatus | undefined {
28e78158
JB
336 if (this.hasEvses) {
337 for (const evseStatus of this.evses.values()) {
338 if (evseStatus.connectors.has(connectorId)) {
66a7748d 339 return evseStatus.connectors.get(connectorId)
28e78158
JB
340 }
341 }
66a7748d 342 return undefined
28e78158 343 }
66a7748d 344 return this.connectors.get(connectorId)
c0560973
JB
345 }
346
66a7748d
JB
347 public getConnectorMaximumAvailablePower (connectorId: number): number {
348 let connectorAmperageLimitationPowerLimit: number | undefined
b47d68d7 349 if (
aa63c9b7 350 this.getAmperageLimitation() != null &&
66a7748d 351 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 352 this.getAmperageLimitation()! < this.stationInfo.maximumAmperage!
b47d68d7 353 ) {
4160ae28 354 connectorAmperageLimitationPowerLimit =
5398cecf 355 (this.stationInfo?.currentOutType === CurrentType.AC
cc6e8ab5 356 ? ACElectricUtils.powerTotal(
66a7748d
JB
357 this.getNumberOfPhases(),
358 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
359 this.stationInfo.voltageOut!,
360 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
361 this.getAmperageLimitation()! *
362 (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors())
363 )
364 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
365 DCElectricUtils.power(this.stationInfo.voltageOut!, this.getAmperageLimitation()!)) /
366 this.powerDivider
cc6e8ab5 367 }
66a7748d
JB
368 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
369 const connectorMaximumPower = this.stationInfo.maximumPower! / this.powerDivider
15068be9 370 const connectorChargingProfilesPowerLimit =
66a7748d 371 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId)
5adf6ca4 372 return min(
ad8537a7 373 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
66a7748d 374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 375 isNaN(connectorAmperageLimitationPowerLimit!)
ad8537a7 376 ? Infinity
66a7748d
JB
377 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
378 connectorAmperageLimitationPowerLimit!,
379 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
380 isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit!
381 )
cc6e8ab5
JB
382 }
383
66a7748d 384 public getTransactionIdTag (transactionId: number): string | undefined {
28e78158
JB
385 if (this.hasEvses) {
386 for (const evseStatus of this.evses.values()) {
387 for (const connectorStatus of evseStatus.connectors.values()) {
388 if (connectorStatus.transactionId === transactionId) {
66a7748d 389 return connectorStatus.transactionIdTag
28e78158
JB
390 }
391 }
392 }
393 } else {
394 for (const connectorId of this.connectors.keys()) {
3fa7f799 395 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
66a7748d 396 return this.getConnectorStatus(connectorId)?.transactionIdTag
28e78158 397 }
c0560973
JB
398 }
399 }
400 }
401
66a7748d
JB
402 public getNumberOfRunningTransactions (): number {
403 let numberOfRunningTransactions = 0
ded57f02 404 if (this.hasEvses) {
3fa7f799
JB
405 for (const [evseId, evseStatus] of this.evses) {
406 if (evseId === 0) {
66a7748d 407 continue
3fa7f799 408 }
ded57f02
JB
409 for (const connectorStatus of evseStatus.connectors.values()) {
410 if (connectorStatus.transactionStarted === true) {
66a7748d 411 ++numberOfRunningTransactions
ded57f02
JB
412 }
413 }
414 }
415 } else {
416 for (const connectorId of this.connectors.keys()) {
417 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
66a7748d 418 ++numberOfRunningTransactions
ded57f02
JB
419 }
420 }
421 }
66a7748d 422 return numberOfRunningTransactions
ded57f02
JB
423 }
424
66a7748d 425 public getConnectorIdByTransactionId (transactionId: number): number | undefined {
28e78158
JB
426 if (this.hasEvses) {
427 for (const evseStatus of this.evses.values()) {
428 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
429 if (connectorStatus.transactionId === transactionId) {
66a7748d 430 return connectorId
28e78158
JB
431 }
432 }
433 }
434 } else {
435 for (const connectorId of this.connectors.keys()) {
3fa7f799 436 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
66a7748d 437 return connectorId
28e78158 438 }
c0560973
JB
439 }
440 }
441 }
442
66a7748d 443 public getEnergyActiveImportRegisterByTransactionId (
07989fad 444 transactionId: number,
66a7748d 445 rounded = false
07989fad
JB
446 ): number {
447 return this.getEnergyActiveImportRegister(
66a7748d 448 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 449 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!)!,
66a7748d
JB
450 rounded
451 )
cbad1217
JB
452 }
453
66a7748d
JB
454 public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
455 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
456 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId)!, rounded)
6ed92bc1
JB
457 }
458
66a7748d 459 public getAuthorizeRemoteTxRequests (): boolean {
f2d5e3d9 460 const authorizeRemoteTxRequests = getConfigurationKey(
17ac262c 461 this,
66a7748d
JB
462 StandardParametersKey.AuthorizeRemoteTxRequests
463 )
a807045b 464 return authorizeRemoteTxRequests != null
4e3b1d6b 465 ? convertToBoolean(authorizeRemoteTxRequests.value)
66a7748d 466 : false
c0560973
JB
467 }
468
66a7748d 469 public getLocalAuthListEnabled (): boolean {
f2d5e3d9 470 const localAuthListEnabled = getConfigurationKey(
17ac262c 471 this,
66a7748d
JB
472 StandardParametersKey.LocalAuthListEnabled
473 )
a807045b 474 return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
c0560973
JB
475 }
476
66a7748d
JB
477 public getHeartbeatInterval (): number {
478 const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)
a807045b 479 if (HeartbeatInterval != null) {
66a7748d 480 return secondsToMilliseconds(convertToInt(HeartbeatInterval.value))
8f953431 481 }
66a7748d 482 const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)
a807045b 483 if (HeartBeatInterval != null) {
66a7748d 484 return secondsToMilliseconds(convertToInt(HeartBeatInterval.value))
8f953431
JB
485 }
486 this.stationInfo?.autoRegister === false &&
487 logger.warn(
488 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
489 Constants.DEFAULT_HEARTBEAT_INTERVAL
66a7748d
JB
490 }`
491 )
492 return Constants.DEFAULT_HEARTBEAT_INTERVAL
8f953431
JB
493 }
494
66a7748d 495 public setSupervisionUrl (url: string): void {
269de583 496 if (
3e888c65 497 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
5398cecf 498 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey)
269de583 499 ) {
66a7748d
JB
500 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
501 setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey!, url)
269de583 502 } else {
66a7748d
JB
503 this.stationInfo.supervisionUrls = url
504 this.saveStationInfo()
505 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
269de583
JB
506 }
507 }
508
66a7748d 509 public startHeartbeat (): void {
a807045b 510 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) {
6a8329b4
JB
511 this.heartbeatSetInterval = setInterval(() => {
512 this.ocppRequestService
513 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
72092cfc 514 .catch((error) => {
6a8329b4
JB
515 logger.error(
516 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
66a7748d
JB
517 error
518 )
519 })
520 }, this.getHeartbeatInterval())
e7aeea18 521 logger.info(
9bf0ef23 522 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
66a7748d
JB
523 this.getHeartbeatInterval()
524 )}`
525 )
a807045b 526 } else if (this.heartbeatSetInterval != null) {
e7aeea18 527 logger.info(
9bf0ef23 528 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
66a7748d
JB
529 this.getHeartbeatInterval()
530 )}`
531 )
c0560973 532 } else {
e7aeea18 533 logger.error(
66a7748d
JB
534 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
535 )
c0560973
JB
536 }
537 }
538
66a7748d 539 public restartHeartbeat (): void {
c0560973 540 // Stop heartbeat
66a7748d 541 this.stopHeartbeat()
c0560973 542 // Start heartbeat
66a7748d 543 this.startHeartbeat()
c0560973
JB
544 }
545
66a7748d 546 public restartWebSocketPing (): void {
17ac262c 547 // Stop WebSocket ping
66a7748d 548 this.stopWebSocketPing()
17ac262c 549 // Start WebSocket ping
66a7748d 550 this.startWebSocketPing()
17ac262c
JB
551 }
552
66a7748d 553 public startMeterValues (connectorId: number, interval: number): void {
c0560973 554 if (connectorId === 0) {
66a7748d
JB
555 logger.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
556 return
c0560973 557 }
66a7748d 558 if (this.getConnectorStatus(connectorId) == null) {
e7aeea18 559 logger.error(
66dd3447 560 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
66a7748d
JB
561 ${connectorId}`
562 )
563 return
c0560973 564 }
5e3cb728 565 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
e7aeea18 566 logger.error(
66a7748d
JB
567 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
568 )
569 return
e7aeea18 570 } else if (
5e3cb728 571 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
aa63c9b7 572 this.getConnectorStatus(connectorId)?.transactionId == null
e7aeea18
JB
573 ) {
574 logger.error(
66a7748d
JB
575 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
576 )
577 return
c0560973
JB
578 }
579 if (interval > 0) {
66a7748d 580 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 581 this.getConnectorStatus(connectorId)!.transactionSetInterval = setInterval(() => {
6a5f5908 582 const meterValue = buildMeterValue(
6a8329b4
JB
583 this,
584 connectorId,
66a7748d 585 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 586 this.getConnectorStatus(connectorId)!.transactionId!,
66a7748d
JB
587 interval
588 )
6a8329b4
JB
589 this.ocppRequestService
590 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
66a7748d
JB
591 this,
592 RequestCommand.METER_VALUES,
593 {
594 connectorId,
595 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
596 meterValue: [meterValue]
597 }
598 )
72092cfc 599 .catch((error) => {
6a8329b4
JB
600 logger.error(
601 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
66a7748d
JB
602 error
603 )
604 })
605 }, interval)
c0560973 606 } else {
e7aeea18
JB
607 logger.error(
608 `${this.logPrefix()} Charging station ${
609 StandardParametersKey.MeterValueSampleInterval
66a7748d
JB
610 } configuration set to ${interval}, not sending MeterValues`
611 )
c0560973
JB
612 }
613 }
614
66a7748d 615 public stopMeterValues (connectorId: number): void {
a807045b 616 if (this.getConnectorStatus(connectorId)?.transactionSetInterval != null) {
66a7748d 617 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval)
04b1261c
JB
618 }
619 }
620
66a7748d
JB
621 public start (): void {
622 if (!this.started) {
623 if (!this.starting) {
624 this.starting = true
5398cecf 625 if (this.stationInfo?.enableStatistics === true) {
66a7748d 626 this.performanceStatistics?.start()
0d8852a5 627 }
66a7748d 628 this.openWSConnection()
0d8852a5 629 // Monitor charging station template file
1f8f6332
JB
630 this.templateFileWatcher = watchJsonFile(
631 this.templateFile,
632 FileType.ChargingStationTemplate,
633 this.logPrefix(),
634 undefined,
635 (event, filename): void => {
636 if (isNotEmptyString(filename) && event === 'change') {
637 try {
638 logger.debug(
639 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
640 this.templateFile
66a7748d
JB
641 } file have changed, reload`
642 )
643 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
1f8f6332 644 // Initialize
66a7748d
JB
645 this.initialize()
646 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
647 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!)
1f8f6332 648 // Restart the ATG
66a7748d
JB
649 this.stopAutomaticTransactionGenerator()
650 delete this.automaticTransactionGeneratorConfiguration
651 if (this.getAutomaticTransactionGeneratorConfiguration().enable) {
652 this.startAutomaticTransactionGenerator()
1f8f6332 653 }
5398cecf 654 if (this.stationInfo?.enableStatistics === true) {
66a7748d 655 this.performanceStatistics?.restart()
1f8f6332 656 } else {
66a7748d 657 this.performanceStatistics?.stop()
1f8f6332
JB
658 }
659 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
660 } catch (error) {
661 logger.error(
662 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
66a7748d
JB
663 error
664 )
1f8f6332
JB
665 }
666 }
66a7748d
JB
667 }
668 )
669 this.started = true
670 this.emit(ChargingStationEvents.started)
671 this.starting = false
0d8852a5 672 } else {
66a7748d 673 logger.warn(`${this.logPrefix()} Charging station is already starting...`)
0d8852a5 674 }
950b1349 675 } else {
66a7748d 676 logger.warn(`${this.logPrefix()} Charging station is already started...`)
950b1349 677 }
c0560973
JB
678 }
679
66a7748d
JB
680 public async stop (reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
681 if (this.started) {
682 if (!this.stopping) {
683 this.stopping = true
684 await this.stopMessageSequence(reason, stopTransactions)
685 this.closeWSConnection()
5398cecf 686 if (this.stationInfo?.enableStatistics === true) {
66a7748d 687 this.performanceStatistics?.stop()
0d8852a5 688 }
66a7748d
JB
689 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
690 this.templateFileWatcher?.close()
691 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
692 delete this.bootNotificationResponse
693 this.started = false
694 this.saveConfiguration()
695 this.emit(ChargingStationEvents.stopped)
696 this.stopping = false
0d8852a5 697 } else {
66a7748d 698 logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
c0560973 699 }
950b1349 700 } else {
66a7748d 701 logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
c0560973 702 }
c0560973
JB
703 }
704
66a7748d
JB
705 public async reset (reason?: StopTransactionReason): Promise<void> {
706 await this.stop(reason)
707 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
708 await sleep(this.stationInfo.resetTime!)
709 this.initialize()
710 this.start()
94ec7e96
JB
711 }
712
66a7748d 713 public saveOcppConfiguration (): void {
5398cecf 714 if (this.stationInfo?.ocppPersistentConfiguration === true) {
66a7748d 715 this.saveConfiguration()
e6895390
JB
716 }
717 }
718
66a7748d
JB
719 public bufferMessage (message: string): void {
720 this.messageBuffer.add(message)
721 this.setIntervalFlushMessageBuffer()
3ba2381e
JB
722 }
723
66a7748d 724 public openWSConnection (
7f3decca 725 options?: WsOptions,
66a7748d 726 params?: { closeOpened?: boolean, terminateOpened?: boolean }
db2336d9 727 ): void {
7f3decca
JB
728 options = {
729 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
e6a33233 730 ...this.stationInfo?.wsOptions,
66a7748d
JB
731 ...options
732 }
733 params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
e6a33233 734 if (!checkChargingStation(this, this.logPrefix())) {
66a7748d 735 return
d1c6c833 736 }
aa63c9b7 737 if (this.stationInfo.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
66a7748d 738 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
db2336d9 739 }
66a7748d
JB
740 if (params?.closeOpened === true) {
741 this.closeWSConnection()
db2336d9 742 }
66a7748d
JB
743 if (params?.terminateOpened === true) {
744 this.terminateWSConnection()
db2336d9 745 }
db2336d9 746
66a7748d 747 if (this.isWebSocketConnectionOpened()) {
0a03f36c 748 logger.warn(
66a7748d
JB
749 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
750 )
751 return
0a03f36c
JB
752 }
753
db2336d9 754 logger.info(
66a7748d
JB
755 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
756 )
db2336d9 757
feff11ec
JB
758 this.wsConnection = new WebSocket(
759 this.wsConnectionUrl,
9a77cc07 760 `ocpp${this.stationInfo?.ocppVersion}`,
66a7748d
JB
761 options
762 )
db2336d9
JB
763
764 // Handle WebSocket message
765 this.wsConnection.on(
766 'message',
66a7748d
JB
767 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
768 )
db2336d9
JB
769 // Handle WebSocket error
770 this.wsConnection.on(
771 'error',
66a7748d
JB
772 this.onError.bind(this) as (this: WebSocket, error: Error) => void
773 )
db2336d9
JB
774 // Handle WebSocket close
775 this.wsConnection.on(
776 'close',
66a7748d
JB
777 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
778 )
db2336d9 779 // Handle WebSocket open
66a7748d 780 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void)
db2336d9 781 // Handle WebSocket ping
66a7748d 782 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void)
db2336d9 783 // Handle WebSocket pong
66a7748d 784 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void)
db2336d9
JB
785 }
786
66a7748d
JB
787 public closeWSConnection (): void {
788 if (this.isWebSocketConnectionOpened()) {
789 this.wsConnection?.close()
790 this.wsConnection = null
db2336d9
JB
791 }
792 }
793
66a7748d 794 public getAutomaticTransactionGeneratorConfiguration (): AutomaticTransactionGeneratorConfiguration {
aa63c9b7 795 if (this.automaticTransactionGeneratorConfiguration == null) {
c7db8ecb 796 let automaticTransactionGeneratorConfiguration:
66a7748d
JB
797 | AutomaticTransactionGeneratorConfiguration
798 | undefined
799 const stationTemplate = this.getTemplateFromFile()
800 const stationConfiguration = this.getConfigurationFromFile()
c7db8ecb 801 if (
5398cecf 802 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
61854f7c 803 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
66a7748d 804 stationConfiguration?.automaticTransactionGenerator != null
c7db8ecb
JB
805 ) {
806 automaticTransactionGeneratorConfiguration =
66a7748d 807 stationConfiguration?.automaticTransactionGenerator
c7db8ecb 808 } else {
66a7748d 809 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
c7db8ecb
JB
810 }
811 this.automaticTransactionGeneratorConfiguration = {
812 ...Constants.DEFAULT_ATG_CONFIGURATION,
66a7748d
JB
813 ...automaticTransactionGeneratorConfiguration
814 }
ac7f79af 815 }
aa63c9b7 816 return this.automaticTransactionGeneratorConfiguration
ac7f79af
JB
817 }
818
66a7748d
JB
819 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
820 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
5ced7e80
JB
821 }
822
66a7748d
JB
823 public startAutomaticTransactionGenerator (connectorIds?: number[]): void {
824 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
9bf0ef23 825 if (isNotEmptyArray(connectorIds)) {
66a7748d 826 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 827 for (const connectorId of connectorIds!) {
66a7748d 828 this.automaticTransactionGenerator?.startConnector(connectorId)
a5e9befc
JB
829 }
830 } else {
66a7748d 831 this.automaticTransactionGenerator?.start()
4f69be04 832 }
66a7748d
JB
833 this.saveAutomaticTransactionGeneratorConfiguration()
834 this.emit(ChargingStationEvents.updated)
4f69be04
JB
835 }
836
66a7748d 837 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
9bf0ef23 838 if (isNotEmptyArray(connectorIds)) {
66a7748d 839 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 840 for (const connectorId of connectorIds!) {
66a7748d 841 this.automaticTransactionGenerator?.stopConnector(connectorId)
a5e9befc
JB
842 }
843 } else {
66a7748d 844 this.automaticTransactionGenerator?.stop()
4f69be04 845 }
66a7748d
JB
846 this.saveAutomaticTransactionGeneratorConfiguration()
847 this.emit(ChargingStationEvents.updated)
4f69be04
JB
848 }
849
66a7748d 850 public async stopTransactionOnConnector (
5e3cb728 851 connectorId: number,
66a7748d 852 reason?: StopTransactionReason
5e3cb728 853 ): Promise<StopTransactionResponse> {
66a7748d 854 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
5e3cb728 855 if (
5398cecf
JB
856 this.stationInfo?.beginEndMeterValues === true &&
857 this.stationInfo?.ocppStrictCompliance === true &&
858 this.stationInfo?.outOfOrderEndMeterValues === false
5e3cb728 859 ) {
41f3983a 860 const transactionEndMeterValue = buildTransactionEndMeterValue(
5e3cb728
JB
861 this,
862 connectorId,
66a7748d
JB
863 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
864 this.getEnergyActiveImportRegisterByTransactionId(transactionId!)
865 )
5e3cb728
JB
866 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
867 this,
868 RequestCommand.METER_VALUES,
869 {
870 connectorId,
871 transactionId,
66a7748d
JB
872 meterValue: [transactionEndMeterValue]
873 }
874 )
5e3cb728 875 }
66a7748d
JB
876 return await this.ocppRequestService.requestHandler<
877 StopTransactionRequest,
878 StopTransactionResponse
879 >(this, RequestCommand.STOP_TRANSACTION, {
880 transactionId,
881 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
882 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
aa63c9b7 883 ...(reason != null && { reason })
66a7748d 884 })
5e3cb728
JB
885 }
886
66a7748d 887 public getReserveConnectorZeroSupported (): boolean {
9bf0ef23 888 return convertToBoolean(
66a7748d
JB
889 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
890 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
891 )
24578c31
JB
892 }
893
66a7748d
JB
894 public async addReservation (reservation: Reservation): Promise<void> {
895 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
a807045b 896 if (reservationFound != null) {
66a7748d 897 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
d193a949 898 }
66a7748d
JB
899 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
900 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
041365be 901 await sendAndSetConnectorStatus(
d193a949 902 this,
ec94a3cf
JB
903 reservation.connectorId,
904 ConnectorStatusEnum.Reserved,
e1d9a0f4 905 undefined,
66a7748d
JB
906 { send: reservation.connectorId !== 0 }
907 )
24578c31
JB
908 }
909
66a7748d 910 public async removeReservation (
d193a949 911 reservation: Reservation,
66a7748d 912 reason: ReservationTerminationReason
d193a949 913 ): Promise<void> {
66a7748d
JB
914 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
915 const connector = this.getConnectorStatus(reservation.connectorId)!
d193a949 916 switch (reason) {
96d96b12 917 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
ec94a3cf 918 case ReservationTerminationReason.TRANSACTION_STARTED:
66a7748d
JB
919 delete connector.reservation
920 break
e74bc549
JB
921 case ReservationTerminationReason.RESERVATION_CANCELED:
922 case ReservationTerminationReason.REPLACE_EXISTING:
923 case ReservationTerminationReason.EXPIRED:
041365be 924 await sendAndSetConnectorStatus(
d193a949 925 this,
ec94a3cf
JB
926 reservation.connectorId,
927 ConnectorStatusEnum.Available,
e1d9a0f4 928 undefined,
66a7748d
JB
929 { send: reservation.connectorId !== 0 }
930 )
931 delete connector.reservation
932 break
b029e74e 933 default:
90aceaf6 934 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
66a7748d 935 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
d193a949 936 }
24578c31
JB
937 }
938
66a7748d 939 public getReservationBy (
366f75f6 940 filterKey: ReservationKey,
66a7748d 941 value: number | string
3fa7f799 942 ): Reservation | undefined {
66dd3447 943 if (this.hasEvses) {
3fa7f799
JB
944 for (const evseStatus of this.evses.values()) {
945 for (const connectorStatus of evseStatus.connectors.values()) {
2ca0ea90 946 if (connectorStatus?.reservation?.[filterKey] === value) {
66a7748d 947 return connectorStatus.reservation
66dd3447
JB
948 }
949 }
950 }
951 } else {
3fa7f799 952 for (const connectorStatus of this.connectors.values()) {
2ca0ea90 953 if (connectorStatus?.reservation?.[filterKey] === value) {
66a7748d 954 return connectorStatus.reservation
66dd3447
JB
955 }
956 }
957 }
d193a949
JB
958 }
959
66a7748d 960 public isConnectorReservable (
e6948a57
JB
961 reservationId: number,
962 idTag?: string,
66a7748d 963 connectorId?: number
e6948a57 964 ): boolean {
66a7748d
JB
965 const reservation = this.getReservationBy('reservationId', reservationId)
966 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
967 const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!)
e6948a57 968 if (arguments.length === 1) {
66a7748d 969 return !reservationExists
e6948a57 970 } else if (arguments.length > 1) {
90aceaf6 971 const userReservation = !isUndefined(idTag)
66a7748d
JB
972 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
973 this.getReservationBy('idTag', idTag!)
974 : undefined
e6948a57 975 const userReservationExists =
66a7748d
JB
976 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
977 !isUndefined(userReservation) && !hasReservationExpired(userReservation!)
978 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
979 const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0
980 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
e6948a57
JB
981 return (
982 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
66a7748d 983 )
e6948a57 984 }
66a7748d 985 return false
e6948a57
JB
986 }
987
66a7748d 988 private setIntervalFlushMessageBuffer (): void {
a807045b 989 if (this.flushMessageBufferSetInterval == null) {
2a2ad81b 990 this.flushMessageBufferSetInterval = setInterval(() => {
66a7748d
JB
991 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
992 this.flushMessageBuffer()
2a2ad81b
JB
993 }
994 if (this.messageBuffer.size === 0) {
66a7748d 995 this.clearIntervalFlushMessageBuffer()
2a2ad81b 996 }
66a7748d 997 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
2a2ad81b
JB
998 }
999 }
1000
66a7748d 1001 private clearIntervalFlushMessageBuffer (): void {
a807045b 1002 if (this.flushMessageBufferSetInterval != null) {
66a7748d
JB
1003 clearInterval(this.flushMessageBufferSetInterval)
1004 delete this.flushMessageBufferSetInterval
2a2ad81b
JB
1005 }
1006 }
1007
66a7748d
JB
1008 private getNumberOfReservableConnectors (): number {
1009 let numberOfReservableConnectors = 0
66dd3447 1010 if (this.hasEvses) {
3fa7f799 1011 for (const evseStatus of this.evses.values()) {
66a7748d 1012 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
66dd3447
JB
1013 }
1014 } else {
66a7748d 1015 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
66dd3447 1016 }
66a7748d 1017 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
66dd3447
JB
1018 }
1019
66a7748d 1020 private getNumberOfReservationsOnConnectorZero (): number {
6913d568 1021 if (
66a7748d
JB
1022 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1023 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
6913d568 1024 ) {
66a7748d 1025 return 1
66dd3447 1026 }
66a7748d 1027 return 0
24578c31
JB
1028 }
1029
66a7748d 1030 private flushMessageBuffer (): void {
8e242273 1031 if (this.messageBuffer.size > 0) {
7d3b0f64 1032 for (const message of this.messageBuffer.values()) {
66a7748d
JB
1033 let beginId: string | undefined
1034 let commandName: RequestCommand | undefined
1035 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1036 const isRequest = messageType === MessageType.CALL_MESSAGE
1431af78 1037 if (isRequest) {
66a7748d
JB
1038 [, , commandName] = JSON.parse(message) as OutgoingRequest
1039 beginId = PerformanceStatistics.beginMeasure(commandName)
1431af78 1040 }
d42379d8 1041 this.wsConnection?.send(message, (error?: Error) => {
66a7748d
JB
1042 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1043 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
aa63c9b7 1044 if (error == null) {
d42379d8 1045 logger.debug(
041365be 1046 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
66a7748d
JB
1047 messageType
1048 )} OCPP message sent '${JSON.stringify(message)}'`
1049 )
1050 this.messageBuffer.delete(message)
041365be
JB
1051 } else {
1052 logger.debug(
1053 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
66a7748d 1054 messageType
041365be 1055 )} OCPP message '${JSON.stringify(message)}' send failed:`,
66a7748d
JB
1056 error
1057 )
d42379d8 1058 }
66a7748d 1059 })
7d3b0f64 1060 }
77f00f84
JB
1061 }
1062 }
1063
66a7748d
JB
1064 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1065 let template: ChargingStationTemplate | undefined
5ad8570f 1066 try {
cda5d0fb 1067 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
66a7748d 1068 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
7c72977b 1069 } else {
66a7748d
JB
1070 const measureId = `${FileType.ChargingStationTemplate} read`
1071 const beginId = PerformanceStatistics.beginMeasure(measureId)
1072 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1073 PerformanceStatistics.endMeasure(measureId, beginId)
d972af76 1074 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
7c72977b 1075 .update(JSON.stringify(template))
66a7748d
JB
1076 .digest('hex')
1077 this.sharedLRUCache.setChargingStationTemplate(template)
1078 this.templateFileHash = template.templateHash
7c72977b 1079 }
5ad8570f 1080 } catch (error) {
fa5995d6 1081 handleFileException(
2484ac1e 1082 this.templateFile,
7164966d
JB
1083 FileType.ChargingStationTemplate,
1084 error as NodeJS.ErrnoException,
66a7748d
JB
1085 this.logPrefix()
1086 )
1087 }
1088 return template
1089 }
1090
1091 private getStationInfoFromTemplate (): ChargingStationInfo {
1092 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1093 const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!
1094 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1095 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1096 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1097 if (stationTemplate?.Connectors != null) {
1098 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1099 }
1100 const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate)
1101 stationInfo.hashId = getHashId(this.index, stationTemplate)
1102 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1103 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16
1104 createSerialNumber(stationTemplate, stationInfo)
1105 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
9bf0ef23 1106 if (isNotEmptyArray(stationTemplate?.power)) {
66a7748d
JB
1107 stationTemplate.power = stationTemplate.power as number[]
1108 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
cc6e8ab5 1109 stationInfo.maximumPower =
72092cfc 1110 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4 1111 ? stationTemplate.power[powerArrayRandomIndex] * 1000
66a7748d 1112 : stationTemplate.power[powerArrayRandomIndex]
5ad8570f 1113 } else {
66a7748d 1114 stationTemplate.power = stationTemplate?.power as number
cc6e8ab5 1115 stationInfo.maximumPower =
72092cfc 1116 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4 1117 ? stationTemplate.power * 1000
66a7748d 1118 : stationTemplate.power
fa7bccf4 1119 }
66a7748d 1120 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
3637ca2c 1121 stationInfo.firmwareVersionPattern =
66a7748d 1122 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
3637ca2c 1123 if (
9bf0ef23 1124 isNotEmptyString(stationInfo.firmwareVersion) &&
66a7748d
JB
1125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1126 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!)
3637ca2c
JB
1127 ) {
1128 logger.warn(
1129 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1130 this.templateFile
66a7748d
JB
1131 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1132 )
3637ca2c 1133 }
598c886d 1134 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 1135 {
598c886d 1136 versionUpgrade: {
66a7748d 1137 step: 1
598c886d 1138 },
66a7748d 1139 reset: true
15748260 1140 },
66a7748d
JB
1141 stationTemplate?.firmwareUpgrade ?? {}
1142 )
aa63c9b7
JB
1143 stationInfo.resetTime =
1144 stationTemplate?.resetTime != null
1145 ? secondsToMilliseconds(stationTemplate.resetTime)
1146 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
66a7748d 1147 return stationInfo
5ad8570f
JB
1148 }
1149
66a7748d
JB
1150 private getStationInfoFromFile (
1151 stationInfoPersistentConfiguration = true
78786898 1152 ): ChargingStationInfo | undefined {
66a7748d
JB
1153 let stationInfo: ChargingStationInfo | undefined
1154 if (stationInfoPersistentConfiguration) {
1155 stationInfo = this.getConfigurationFromFile()?.stationInfo
1156 if (stationInfo != null) {
1157 delete stationInfo?.infoHash
f832e5df
JB
1158 }
1159 }
66a7748d 1160 return stationInfo
2484ac1e
JB
1161 }
1162
66a7748d
JB
1163 private getStationInfo (): ChargingStationInfo {
1164 const defaultStationInfo = Constants.DEFAULT_STATION_INFO
1165 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate()
78786898 1166 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile(
66a7748d
JB
1167 stationInfoFromTemplate?.stationInfoPersistentConfiguration
1168 )
6b90dcca
JB
1169 // Priority:
1170 // 1. charging station info from template
1171 // 2. charging station info from configuration file
f765beaa 1172 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
66a7748d
JB
1173 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1174 return { ...defaultStationInfo, ...stationInfoFromFile! }
f765beaa 1175 }
66a7748d 1176 stationInfoFromFile != null &&
fba11dc6 1177 propagateSerialNumber(
66a7748d 1178 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 1179 this.getTemplateFromFile()!,
fec4d204 1180 stationInfoFromFile,
66a7748d
JB
1181 stationInfoFromTemplate
1182 )
1183 return { ...defaultStationInfo, ...stationInfoFromTemplate }
2484ac1e
JB
1184 }
1185
66a7748d 1186 private saveStationInfo (): void {
5398cecf 1187 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
66a7748d 1188 this.saveConfiguration()
ccb1d6e9 1189 }
2484ac1e
JB
1190 }
1191
66a7748d
JB
1192 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1193 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1194 logger.error(`${this.logPrefix()} ${errorMsg}`)
1195 throw new BaseError(errorMsg)
c0560973
JB
1196 }
1197
66a7748d
JB
1198 private initialize (): void {
1199 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1200 const stationTemplate = this.getTemplateFromFile()!
1201 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
d972af76
JB
1202 this.configurationFile = join(
1203 dirname(this.templateFile.replace('station-templates', 'configurations')),
66a7748d
JB
1204 `${getHashId(this.index, stationTemplate)}.json`
1205 )
1206 const stationConfiguration = this.getConfigurationFromFile()
a4f7c75f 1207 if (
61854f7c 1208 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
e1d9a0f4 1209 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
66a7748d 1210 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
a4f7c75f 1211 ) {
66a7748d
JB
1212 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1213 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
a4f7c75f 1214 } else {
66a7748d 1215 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
a4f7c75f 1216 }
66a7748d 1217 this.stationInfo = this.getStationInfo()
3637ca2c
JB
1218 if (
1219 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
9bf0ef23
JB
1220 isNotEmptyString(this.stationInfo.firmwareVersion) &&
1221 isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1222 ) {
d812bdcb 1223 const patternGroup: number | undefined =
15748260 1224 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
66a7748d
JB
1225 this.stationInfo.firmwareVersion?.split('.').length
1226 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
9e026a6a 1227 const match = new RegExp(this.stationInfo.firmwareVersionPattern!)
66a7748d 1228 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
77807350 1229 .exec(this.stationInfo.firmwareVersion!)
66a7748d
JB
1230 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1231 ?.slice(1, patternGroup! + 1)
aa63c9b7
JB
1232 if (match != null) {
1233 const patchLevelIndex = match.length - 1
1234 match[patchLevelIndex] = (
1235 convertToInt(match[patchLevelIndex]) +
1236 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1237 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1238 ).toString()
1239 this.stationInfo.firmwareVersion = match.join('.')
77807350 1240 }
3637ca2c 1241 }
66a7748d
JB
1242 this.saveStationInfo()
1243 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
5398cecf 1244 if (this.stationInfo?.enableStatistics === true) {
6bccfcbc
JB
1245 this.performanceStatistics = PerformanceStatistics.getInstance(
1246 this.stationInfo.hashId,
66a7748d 1247 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 1248 this.stationInfo.chargingStationId!,
66a7748d
JB
1249 this.configuredSupervisionUrl
1250 )
6bccfcbc 1251 }
66a7748d
JB
1252 this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1253 this.powerDivider = this.getPowerDivider()
692f2f64 1254 // OCPP configuration
66a7748d
JB
1255 this.ocppConfiguration = this.getOcppConfiguration()
1256 this.initializeOcppConfiguration()
1257 this.initializeOcppServices()
3e888c65
JB
1258 this.once(ChargingStationEvents.accepted, () => {
1259 this.startMessageSequence().catch((error) => {
66a7748d
JB
1260 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
1261 })
1262 })
692f2f64
JB
1263 if (this.stationInfo?.autoRegister === true) {
1264 this.bootNotificationResponse = {
1265 currentTime: new Date(),
be4c6702 1266 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
66a7748d
JB
1267 status: RegistrationStatusEnumType.ACCEPTED
1268 }
692f2f64 1269 }
147d0e0f
JB
1270 }
1271
66a7748d
JB
1272 private initializeOcppServices (): void {
1273 const ocppVersion = this.stationInfo?.ocppVersion
feff11ec
JB
1274 switch (ocppVersion) {
1275 case OCPPVersion.VERSION_16:
1276 this.ocppIncomingRequestService =
66a7748d 1277 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
feff11ec 1278 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
66a7748d
JB
1279 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1280 )
1281 break
feff11ec
JB
1282 case OCPPVersion.VERSION_20:
1283 case OCPPVersion.VERSION_201:
1284 this.ocppIncomingRequestService =
66a7748d 1285 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
feff11ec 1286 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
66a7748d
JB
1287 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1288 )
1289 break
feff11ec 1290 default:
66a7748d
JB
1291 this.handleUnsupportedVersion(ocppVersion)
1292 break
feff11ec
JB
1293 }
1294 }
1295
66a7748d 1296 private initializeOcppConfiguration (): void {
aa63c9b7 1297 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
66a7748d 1298 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
f0f65a62 1299 }
aa63c9b7 1300 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
66a7748d 1301 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
f0f65a62 1302 }
e7aeea18 1303 if (
4e3b1d6b 1304 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
5398cecf 1305 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
66a7748d 1306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
aa63c9b7 1307 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) == null
e7aeea18 1308 ) {
f2d5e3d9 1309 addConfigurationKey(
17ac262c 1310 this,
66a7748d 1311 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5398cecf 1312 this.stationInfo.supervisionUrlOcppKey!,
fa7bccf4 1313 this.configuredSupervisionUrl.href,
66a7748d
JB
1314 { reboot: true }
1315 )
e6895390 1316 } else if (
4e3b1d6b 1317 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
5398cecf 1318 isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) &&
66a7748d 1319 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
aa63c9b7 1320 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) != null
e6895390 1321 ) {
66a7748d
JB
1322 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1323 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false })
12fc74d6 1324 }
cc6e8ab5 1325 if (
9bf0ef23 1326 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
66a7748d 1327 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
aa63c9b7 1328 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) == null
cc6e8ab5 1329 ) {
f2d5e3d9 1330 addConfigurationKey(
17ac262c 1331 this,
66a7748d 1332 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 1333 this.stationInfo.amperageLimitationOcppKey!,
66a7748d
JB
1334 // prettier-ignore
1335 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1336 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1337 )
cc6e8ab5 1338 }
aa63c9b7 1339 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
f2d5e3d9 1340 addConfigurationKey(
17ac262c 1341 this,
e7aeea18 1342 StandardParametersKey.SupportedFeatureProfiles,
66a7748d
JB
1343 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1344 )
e7aeea18 1345 }
f2d5e3d9 1346 addConfigurationKey(
17ac262c 1347 this,
e7aeea18
JB
1348 StandardParametersKey.NumberOfConnectors,
1349 this.getNumberOfConnectors().toString(),
a95873d8 1350 { readonly: true },
66a7748d
JB
1351 { overwrite: true }
1352 )
aa63c9b7 1353 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
f2d5e3d9 1354 addConfigurationKey(
17ac262c 1355 this,
e7aeea18 1356 StandardParametersKey.MeterValuesSampledData,
66a7748d
JB
1357 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1358 )
7abfea5f 1359 }
aa63c9b7 1360 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
66a7748d 1361 const connectorsPhaseRotation: string[] = []
28e78158
JB
1362 if (this.hasEvses) {
1363 for (const evseStatus of this.evses.values()) {
1364 for (const connectorId of evseStatus.connectors.keys()) {
dd08d43d 1365 connectorsPhaseRotation.push(
66a7748d
JB
1366 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1367 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1368 )
28e78158
JB
1369 }
1370 }
1371 } else {
1372 for (const connectorId of this.connectors.keys()) {
dd08d43d 1373 connectorsPhaseRotation.push(
66a7748d
JB
1374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1375 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1376 )
7e1dc878
JB
1377 }
1378 }
f2d5e3d9 1379 addConfigurationKey(
17ac262c 1380 this,
e7aeea18 1381 StandardParametersKey.ConnectorPhaseRotation,
66a7748d
JB
1382 connectorsPhaseRotation.toString()
1383 )
7e1dc878 1384 }
aa63c9b7 1385 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
66a7748d 1386 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
36f6a92e 1387 }
17ac262c 1388 if (
aa63c9b7 1389 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
a807045b 1390 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
17ac262c 1391 ) {
66a7748d 1392 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
f2d5e3d9 1393 }
aa63c9b7 1394 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
f2d5e3d9 1395 addConfigurationKey(
17ac262c 1396 this,
e7aeea18 1397 StandardParametersKey.ConnectionTimeOut,
66a7748d
JB
1398 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1399 )
8bce55bf 1400 }
66a7748d 1401 this.saveOcppConfiguration()
073bd098
JB
1402 }
1403
66a7748d
JB
1404 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1405 if (configuration?.connectorsStatus != null && configuration?.evsesStatus == null) {
8df5ae48 1406 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
66a7748d 1407 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
8df5ae48 1408 }
66a7748d 1409 } else if (configuration?.evsesStatus != null && configuration?.connectorsStatus == null) {
a4f7c75f 1410 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
66a7748d
JB
1411 const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration)
1412 delete evseStatus.connectorsStatus
a4f7c75f 1413 this.evses.set(evseId, {
8df5ae48 1414 ...(evseStatus as EvseStatus),
a4f7c75f 1415 connectors: new Map<number, ConnectorStatus>(
66a7748d 1416 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
e1d9a0f4 1417 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
a4f7c75f 1418 connectorId,
66a7748d
JB
1419 connectorStatus
1420 ])
1421 )
1422 })
a4f7c75f 1423 }
66a7748d
JB
1424 } else if (configuration?.evsesStatus != null && configuration?.connectorsStatus != null) {
1425 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1426 logger.error(`${this.logPrefix()} ${errorMsg}`)
1427 throw new BaseError(errorMsg)
a4f7c75f 1428 } else {
66a7748d
JB
1429 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1430 logger.error(`${this.logPrefix()} ${errorMsg}`)
1431 throw new BaseError(errorMsg)
a4f7c75f
JB
1432 }
1433 }
1434
66a7748d
JB
1435 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1436 if (stationTemplate?.Connectors != null && stationTemplate?.Evses == null) {
1437 this.initializeConnectorsFromTemplate(stationTemplate)
1438 } else if (stationTemplate?.Evses != null && stationTemplate?.Connectors == null) {
1439 this.initializeEvsesFromTemplate(stationTemplate)
1440 } else if (stationTemplate?.Evses != null && stationTemplate?.Connectors != null) {
1441 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1442 logger.error(`${this.logPrefix()} ${errorMsg}`)
1443 throw new BaseError(errorMsg)
ae25f265 1444 } else {
66a7748d
JB
1445 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1446 logger.error(`${this.logPrefix()} ${errorMsg}`)
1447 throw new BaseError(errorMsg)
ae25f265
JB
1448 }
1449 }
1450
66a7748d
JB
1451 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1452 if (stationTemplate?.Connectors == null && this.connectors.size === 0) {
1453 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1454 logger.error(`${this.logPrefix()} ${errorMsg}`)
1455 throw new BaseError(errorMsg)
3d25cc86 1456 }
66a7748d 1457 if (stationTemplate?.Connectors?.[0] == null) {
3d25cc86
JB
1458 logger.warn(
1459 `${this.logPrefix()} Charging station information from template ${
1460 this.templateFile
66a7748d
JB
1461 } with no connector id 0 configuration`
1462 )
3d25cc86 1463 }
66a7748d 1464 if (stationTemplate?.Connectors != null) {
cda5d0fb 1465 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
66a7748d 1466 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
d972af76 1467 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
cda5d0fb 1468 .update(
66a7748d 1469 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
cda5d0fb 1470 )
66a7748d 1471 .digest('hex')
3d25cc86 1472 const connectorsConfigChanged =
66a7748d 1473 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
3d25cc86 1474 if (this.connectors?.size === 0 || connectorsConfigChanged) {
66a7748d
JB
1475 connectorsConfigChanged && this.connectors.clear()
1476 this.connectorsConfigurationHash = connectorsConfigHash
269196a8
JB
1477 if (templateMaxConnectors > 0) {
1478 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1479 if (
1480 connectorId === 0 &&
66a7748d
JB
1481 (stationTemplate?.Connectors?.[connectorId] == null ||
1482 !this.getUseConnectorId0(stationTemplate))
269196a8 1483 ) {
66a7748d 1484 continue
269196a8
JB
1485 }
1486 const templateConnectorId =
66a7748d 1487 connectorId > 0 && stationTemplate?.randomConnectors === true
9bf0ef23 1488 ? getRandomInteger(templateMaxAvailableConnectors, 1)
66a7748d
JB
1489 : connectorId
1490 const connectorStatus = stationTemplate?.Connectors[templateConnectorId]
fba11dc6 1491 checkStationInfoConnectorStatus(
ae25f265 1492 templateConnectorId,
04b1261c
JB
1493 connectorStatus,
1494 this.logPrefix(),
66a7748d
JB
1495 this.templateFile
1496 )
1497 this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
3d25cc86 1498 }
66a7748d
JB
1499 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1500 this.saveConnectorsStatus()
ae25f265
JB
1501 } else {
1502 logger.warn(
1503 `${this.logPrefix()} Charging station information from template ${
1504 this.templateFile
66a7748d
JB
1505 } with no connectors configuration defined, cannot create connectors`
1506 )
3d25cc86
JB
1507 }
1508 }
1509 } else {
1510 logger.warn(
1511 `${this.logPrefix()} Charging station information from template ${
1512 this.templateFile
66a7748d
JB
1513 } with no connectors configuration defined, using already defined connectors`
1514 )
3d25cc86 1515 }
3d25cc86
JB
1516 }
1517
66a7748d
JB
1518 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1519 if (stationTemplate?.Evses == null && this.evses.size === 0) {
1520 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1521 logger.error(`${this.logPrefix()} ${errorMsg}`)
1522 throw new BaseError(errorMsg)
2585c6e9 1523 }
66a7748d 1524 if (stationTemplate?.Evses?.[0] == null) {
2585c6e9
JB
1525 logger.warn(
1526 `${this.logPrefix()} Charging station information from template ${
1527 this.templateFile
66a7748d
JB
1528 } with no evse id 0 configuration`
1529 )
2585c6e9 1530 }
66a7748d 1531 if (stationTemplate?.Evses?.[0]?.Connectors?.[0] == null) {
59a0f26d
JB
1532 logger.warn(
1533 `${this.logPrefix()} Charging station information from template ${
1534 this.templateFile
66a7748d
JB
1535 } with evse id 0 with no connector id 0 configuration`
1536 )
59a0f26d 1537 }
491dad29
JB
1538 if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) {
1539 logger.warn(
1540 `${this.logPrefix()} Charging station information from template ${
1541 this.templateFile
66a7748d
JB
1542 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1543 )
491dad29 1544 }
66a7748d 1545 if (stationTemplate?.Evses != null) {
d972af76 1546 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
ba01a213 1547 .update(JSON.stringify(stationTemplate?.Evses))
66a7748d 1548 .digest('hex')
2585c6e9 1549 const evsesConfigChanged =
66a7748d 1550 this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
2585c6e9 1551 if (this.evses?.size === 0 || evsesConfigChanged) {
66a7748d
JB
1552 evsesConfigChanged && this.evses.clear()
1553 this.evsesConfigurationHash = evsesConfigHash
1554 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses)
ae25f265 1555 if (templateMaxEvses > 0) {
eb979012 1556 for (const evseKey in stationTemplate.Evses) {
66a7748d 1557 const evseId = convertToInt(evseKey)
52952bf8 1558 this.evses.set(evseId, {
fba11dc6 1559 connectors: buildConnectorsMap(
eb979012 1560 stationTemplate?.Evses[evseKey]?.Connectors,
ae25f265 1561 this.logPrefix(),
66a7748d 1562 this.templateFile
ae25f265 1563 ),
66a7748d
JB
1564 availability: AvailabilityType.Operative
1565 })
1566 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1567 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
ae25f265 1568 }
66a7748d 1569 this.saveEvsesStatus()
ae25f265
JB
1570 } else {
1571 logger.warn(
1572 `${this.logPrefix()} Charging station information from template ${
04b1261c 1573 this.templateFile
66a7748d
JB
1574 } with no evses configuration defined, cannot create evses`
1575 )
2585c6e9
JB
1576 }
1577 }
513db108
JB
1578 } else {
1579 logger.warn(
1580 `${this.logPrefix()} Charging station information from template ${
1581 this.templateFile
66a7748d
JB
1582 } with no evses configuration defined, using already defined evses`
1583 )
2585c6e9
JB
1584 }
1585 }
1586
66a7748d
JB
1587 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1588 let configuration: ChargingStationConfiguration | undefined
9bf0ef23 1589 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
073bd098 1590 try {
57adbebc
JB
1591 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1592 configuration = this.sharedLRUCache.getChargingStationConfiguration(
66a7748d
JB
1593 this.configurationFileHash
1594 )
7c72977b 1595 } else {
66a7748d
JB
1596 const measureId = `${FileType.ChargingStationConfiguration} read`
1597 const beginId = PerformanceStatistics.beginMeasure(measureId)
7c72977b 1598 configuration = JSON.parse(
66a7748d
JB
1599 readFileSync(this.configurationFile, 'utf8')
1600 ) as ChargingStationConfiguration
1601 PerformanceStatistics.endMeasure(measureId, beginId)
1602 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1603 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1604 this.configurationFileHash = configuration.configurationHash!
7c72977b 1605 }
073bd098 1606 } catch (error) {
fa5995d6 1607 handleFileException(
073bd098 1608 this.configurationFile,
7164966d
JB
1609 FileType.ChargingStationConfiguration,
1610 error as NodeJS.ErrnoException,
66a7748d
JB
1611 this.logPrefix()
1612 )
073bd098
JB
1613 }
1614 }
66a7748d 1615 return configuration
073bd098
JB
1616 }
1617
66a7748d 1618 private saveAutomaticTransactionGeneratorConfiguration (): void {
5398cecf 1619 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
66a7748d 1620 this.saveConfiguration()
5ced7e80 1621 }
ac7f79af
JB
1622 }
1623
66a7748d
JB
1624 private saveConnectorsStatus (): void {
1625 this.saveConfiguration()
52952bf8
JB
1626 }
1627
66a7748d
JB
1628 private saveEvsesStatus (): void {
1629 this.saveConfiguration()
52952bf8
JB
1630 }
1631
66a7748d 1632 private saveConfiguration (): void {
9bf0ef23 1633 if (isNotEmptyString(this.configurationFile)) {
2484ac1e 1634 try {
d972af76 1635 if (!existsSync(dirname(this.configurationFile))) {
66a7748d 1636 mkdirSync(dirname(this.configurationFile), { recursive: true })
073bd098 1637 }
66a7748d
JB
1638 let configurationData: ChargingStationConfiguration =
1639 this.getConfigurationFromFile() != null
1640 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1641 cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()!)
1642 : {}
1643 if (
1644 this.stationInfo?.stationInfoPersistentConfiguration === true &&
1645 this.stationInfo != null
1646 ) {
1647 configurationData.stationInfo = this.stationInfo
5ced7e80 1648 } else {
66a7748d 1649 delete configurationData.stationInfo
52952bf8 1650 }
5398cecf
JB
1651 if (
1652 this.stationInfo?.ocppPersistentConfiguration === true &&
755a76d5 1653 Array.isArray(this.ocppConfiguration?.configurationKey)
5398cecf 1654 ) {
66a7748d 1655 configurationData.configurationKey = this.ocppConfiguration?.configurationKey
5ced7e80 1656 } else {
66a7748d 1657 delete configurationData.configurationKey
52952bf8 1658 }
179ed367
JB
1659 configurationData = merge<ChargingStationConfiguration>(
1660 configurationData,
66a7748d
JB
1661 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1662 )
5ced7e80 1663 if (
66a7748d
JB
1664 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
1665 this.getAutomaticTransactionGeneratorConfiguration() == null
5ced7e80 1666 ) {
66a7748d 1667 delete configurationData.automaticTransactionGenerator
5ced7e80 1668 }
b1bbdae5 1669 if (this.connectors.size > 0) {
66a7748d 1670 configurationData.connectorsStatus = buildConnectorsStatus(this)
5ced7e80 1671 } else {
66a7748d 1672 delete configurationData.connectorsStatus
52952bf8 1673 }
b1bbdae5 1674 if (this.evses.size > 0) {
66a7748d 1675 configurationData.evsesStatus = buildEvsesStatus(this)
5ced7e80 1676 } else {
66a7748d 1677 delete configurationData.evsesStatus
52952bf8 1678 }
66a7748d 1679 delete configurationData.configurationHash
d972af76 1680 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
5ced7e80
JB
1681 .update(
1682 JSON.stringify({
1683 stationInfo: configurationData.stationInfo,
1684 configurationKey: configurationData.configurationKey,
1685 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
8ab96efb 1686 ...(this.connectors.size > 0 && {
66a7748d 1687 connectorsStatus: configurationData.connectorsStatus
8ab96efb 1688 }),
66a7748d
JB
1689 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1690 } satisfies ChargingStationConfiguration)
5ced7e80 1691 )
66a7748d 1692 .digest('hex')
7c72977b 1693 if (this.configurationFileHash !== configurationHash) {
0ebf7c2e 1694 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
66a7748d
JB
1695 configurationData.configurationHash = configurationHash
1696 const measureId = `${FileType.ChargingStationConfiguration} write`
1697 const beginId = PerformanceStatistics.beginMeasure(measureId)
0ebf7c2e
JB
1698 writeFileSync(
1699 this.configurationFile,
4ed03b6e 1700 JSON.stringify(configurationData, undefined, 2),
66a7748d
JB
1701 'utf8'
1702 )
1703 PerformanceStatistics.endMeasure(measureId, beginId)
1704 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1705 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1706 this.configurationFileHash = configurationHash
0ebf7c2e
JB
1707 }).catch((error) => {
1708 handleFileException(
1709 this.configurationFile,
1710 FileType.ChargingStationConfiguration,
1711 error as NodeJS.ErrnoException,
66a7748d
JB
1712 this.logPrefix()
1713 )
1714 })
7c72977b
JB
1715 } else {
1716 logger.debug(
1717 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1718 this.configurationFile
66a7748d
JB
1719 }`
1720 )
2484ac1e 1721 }
2484ac1e 1722 } catch (error) {
fa5995d6 1723 handleFileException(
2484ac1e 1724 this.configurationFile,
7164966d
JB
1725 FileType.ChargingStationConfiguration,
1726 error as NodeJS.ErrnoException,
66a7748d
JB
1727 this.logPrefix()
1728 )
073bd098 1729 }
2484ac1e
JB
1730 } else {
1731 logger.error(
66a7748d
JB
1732 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1733 )
073bd098
JB
1734 }
1735 }
1736
66a7748d
JB
1737 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1738 return this.getTemplateFromFile()?.Configuration
2484ac1e
JB
1739 }
1740
66a7748d
JB
1741 private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
1742 const configurationKey = this.getConfigurationFromFile()?.configurationKey
9fe79a13 1743 if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
66a7748d 1744 return { configurationKey }
648512ce 1745 }
66a7748d 1746 return undefined
7dde0b73
JB
1747 }
1748
66a7748d 1749 private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
551e477c 1750 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
66a7748d
JB
1751 this.getOcppConfigurationFromFile()
1752 if (ocppConfiguration == null) {
1753 ocppConfiguration = this.getOcppConfigurationFromTemplate()
2484ac1e 1754 }
66a7748d 1755 return ocppConfiguration
2484ac1e
JB
1756 }
1757
66a7748d
JB
1758 private async onOpen (): Promise<void> {
1759 if (this.isWebSocketConnectionOpened()) {
5144f4d1 1760 logger.info(
66a7748d
JB
1761 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1762 )
1763 let registrationRetryCount = 0
1764 if (!this.isRegistered()) {
5144f4d1 1765 // Send BootNotification
5144f4d1 1766 do {
f7f98c68 1767 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
66a7748d
JB
1768 BootNotificationRequest,
1769 BootNotificationResponse
8bfbc743 1770 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
66a7748d
JB
1771 skipBufferingOnError: true
1772 })
1773 if (!this.isRegistered()) {
1774 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
9bf0ef23 1775 await sleep(
66a7748d 1776 this?.bootNotificationResponse?.interval != null
be4c6702 1777 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
66a7748d
JB
1778 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1779 )
5144f4d1
JB
1780 }
1781 } while (
66a7748d
JB
1782 !this.isRegistered() &&
1783 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5398cecf 1784 (registrationRetryCount <= this.stationInfo.registrationMaxRetries! ||
9a77cc07 1785 this.stationInfo?.registrationMaxRetries === -1)
66a7748d 1786 )
5144f4d1 1787 }
66a7748d
JB
1788 if (this.isRegistered()) {
1789 this.emit(ChargingStationEvents.registered)
1790 if (this.inAcceptedState()) {
1791 this.emit(ChargingStationEvents.accepted)
c0560973 1792 }
5144f4d1
JB
1793 } else {
1794 logger.error(
f23be6aa 1795 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${this
66a7748d
JB
1796 .stationInfo?.registrationMaxRetries})`
1797 )
caad9d6b 1798 }
66a7748d
JB
1799 this.autoReconnectRetryCount = 0
1800 this.emit(ChargingStationEvents.updated)
2e6f5966 1801 } else {
5144f4d1 1802 logger.warn(
66a7748d
JB
1803 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1804 )
2e6f5966 1805 }
2e6f5966
JB
1806 }
1807
66a7748d 1808 private async onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): Promise<void> {
d09085e9 1809 switch (code) {
6c65a295
JB
1810 // Normal close
1811 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1812 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1813 logger.info(
9bf0ef23 1814 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
66a7748d
JB
1815 code
1816 )}' and reason '${reason.toString()}'`
1817 )
1818 this.autoReconnectRetryCount = 0
1819 break
6c65a295
JB
1820 // Abnormal close
1821 default:
e7aeea18 1822 logger.error(
9bf0ef23 1823 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
66a7748d
JB
1824 code
1825 )}' and reason '${reason.toString()}'`
1826 )
1827 this.started && (await this.reconnect())
1828 break
c0560973 1829 }
66a7748d 1830 this.emit(ChargingStationEvents.updated)
2e6f5966
JB
1831 }
1832
66a7748d
JB
1833 private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
1834 const cachedRequest = this.requests.get(messageId)
1835 if (Array.isArray(cachedRequest)) {
1836 return cachedRequest
56d09fd7
JB
1837 }
1838 throw new OCPPError(
1839 ErrorType.PROTOCOL_ERROR,
041365be 1840 `Cached request for message id ${messageId} ${getMessageTypeString(
66a7748d 1841 messageType
56d09fd7
JB
1842 )} is not an array`,
1843 undefined,
66a7748d
JB
1844 cachedRequest
1845 )
56d09fd7
JB
1846 }
1847
66a7748d
JB
1848 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1849 const [messageType, messageId, commandName, commandPayload] = request
9a77cc07 1850 if (this.stationInfo?.enableStatistics === true) {
66a7748d 1851 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
56d09fd7
JB
1852 }
1853 logger.debug(
1854 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
66a7748d
JB
1855 request
1856 )}`
1857 )
56d09fd7
JB
1858 // Process the message
1859 await this.ocppIncomingRequestService.incomingRequestHandler(
1860 this,
1861 messageId,
1862 commandName,
66a7748d
JB
1863 commandPayload
1864 )
1865 this.emit(ChargingStationEvents.updated)
56d09fd7
JB
1866 }
1867
66a7748d
JB
1868 private handleResponseMessage (response: Response): void {
1869 const [messageType, messageId, commandPayload] = response
1870 if (!this.requests.has(messageId)) {
56d09fd7
JB
1871 // Error
1872 throw new OCPPError(
1873 ErrorType.INTERNAL_ERROR,
1874 `Response for unknown message id ${messageId}`,
1875 undefined,
66a7748d
JB
1876 commandPayload
1877 )
56d09fd7
JB
1878 }
1879 // Respond
66a7748d 1880 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
56d09fd7
JB
1881 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1882 messageType,
66a7748d
JB
1883 messageId
1884 )!
56d09fd7
JB
1885 logger.debug(
1886 `${this.logPrefix()} << Command '${
1887 requestCommandName ?? Constants.UNKNOWN_COMMAND
66a7748d
JB
1888 }' received response payload: ${JSON.stringify(response)}`
1889 )
1890 responseCallback(commandPayload, requestPayload)
56d09fd7
JB
1891 }
1892
66a7748d
JB
1893 private handleErrorMessage (errorResponse: ErrorResponse): void {
1894 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1895 if (!this.requests.has(messageId)) {
56d09fd7
JB
1896 // Error
1897 throw new OCPPError(
1898 ErrorType.INTERNAL_ERROR,
1899 `Error response for unknown message id ${messageId}`,
1900 undefined,
66a7748d
JB
1901 { errorType, errorMessage, errorDetails }
1902 )
56d09fd7 1903 }
66a7748d
JB
1904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1905 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
56d09fd7
JB
1906 logger.debug(
1907 `${this.logPrefix()} << Command '${
1908 requestCommandName ?? Constants.UNKNOWN_COMMAND
66a7748d
JB
1909 }' received error response payload: ${JSON.stringify(errorResponse)}`
1910 )
1911 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
56d09fd7
JB
1912 }
1913
66a7748d
JB
1914 private async onMessage (data: RawData): Promise<void> {
1915 let request: IncomingRequest | Response | ErrorResponse | undefined
1916 let messageType: MessageType | undefined
1917 let errorMsg: string
c0560973 1918 try {
e1d9a0f4 1919 // eslint-disable-next-line @typescript-eslint/no-base-to-string
66a7748d
JB
1920 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1921 if (Array.isArray(request)) {
1922 [messageType] = request
b3ec7bc1
JB
1923 // Check the type of message
1924 switch (messageType) {
1925 // Incoming Message
1926 case MessageType.CALL_MESSAGE:
66a7748d
JB
1927 await this.handleIncomingMessage(request as IncomingRequest)
1928 break
56d09fd7 1929 // Response Message
b3ec7bc1 1930 case MessageType.CALL_RESULT_MESSAGE:
66a7748d
JB
1931 this.handleResponseMessage(request as Response)
1932 break
a2d1c0f1
JB
1933 // Error Message
1934 case MessageType.CALL_ERROR_MESSAGE:
66a7748d
JB
1935 this.handleErrorMessage(request as ErrorResponse)
1936 break
56d09fd7 1937 // Unknown Message
b3ec7bc1
JB
1938 default:
1939 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
66a7748d
JB
1940 errorMsg = `Wrong message type ${messageType}`
1941 logger.error(`${this.logPrefix()} ${errorMsg}`)
1942 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
b3ec7bc1 1943 }
47e22477 1944 } else {
e1d9a0f4
JB
1945 throw new OCPPError(
1946 ErrorType.PROTOCOL_ERROR,
1947 'Incoming message is not an array',
1948 undefined,
1949 {
66a7748d
JB
1950 request
1951 }
1952 )
47e22477 1953 }
c0560973 1954 } catch (error) {
66a7748d
JB
1955 let commandName: IncomingRequestCommand | undefined
1956 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
1957 let errorCallback: ErrorCallback
1958 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1959 const [, messageId] = request!
13701f69
JB
1960 switch (messageType) {
1961 case MessageType.CALL_MESSAGE:
66a7748d 1962 [, , commandName] = request as IncomingRequest
13701f69 1963 // Send error
66a7748d
JB
1964 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
1965 break
13701f69
JB
1966 case MessageType.CALL_RESULT_MESSAGE:
1967 case MessageType.CALL_ERROR_MESSAGE:
66a7748d
JB
1968 if (this.requests.has(messageId)) {
1969 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1970 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
13701f69 1971 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
66a7748d 1972 errorCallback(error as OCPPError, false)
13701f69
JB
1973 } else {
1974 // Remove the request from the cache in case of error at response handling
66a7748d 1975 this.requests.delete(messageId)
13701f69 1976 }
66a7748d 1977 break
ba7965c4 1978 }
66a7748d 1979 if (!(error instanceof OCPPError)) {
56d09fd7
JB
1980 logger.warn(
1981 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1982 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 1983 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7 1984 }' message '${data.toString()}' handling is not an OCPPError:`,
66a7748d
JB
1985 error
1986 )
56d09fd7
JB
1987 }
1988 logger.error(
1989 `${this.logPrefix()} Incoming OCPP command '${
1990 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
e1d9a0f4 1991 // eslint-disable-next-line @typescript-eslint/no-base-to-string
56d09fd7
JB
1992 }' message '${data.toString()}'${
1993 messageType !== MessageType.CALL_MESSAGE
1994 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1995 : ''
1996 } processing error:`,
66a7748d
JB
1997 error
1998 )
c0560973 1999 }
2328be1e
JB
2000 }
2001
66a7748d
JB
2002 private onPing (): void {
2003 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
c0560973
JB
2004 }
2005
66a7748d
JB
2006 private onPong (): void {
2007 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
c0560973
JB
2008 }
2009
66a7748d
JB
2010 private onError (error: WSError): void {
2011 this.closeWSConnection()
2012 logger.error(`${this.logPrefix()} WebSocket error:`, error)
c0560973
JB
2013 }
2014
66a7748d 2015 private getEnergyActiveImportRegister (connectorStatus: ConnectorStatus, rounded = false): number {
5398cecf 2016 if (this.stationInfo?.meteringPerTransaction === true) {
07989fad 2017 return (
66a7748d
JB
2018 (rounded
2019 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2020 Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!)
07989fad 2021 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
66a7748d 2022 )
07989fad
JB
2023 }
2024 return (
66a7748d
JB
2025 (rounded
2026 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2027 Math.round(connectorStatus.energyActiveImportRegisterValue!)
07989fad 2028 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
66a7748d 2029 )
07989fad
JB
2030 }
2031
66a7748d
JB
2032 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2033 return stationTemplate?.useConnectorId0 ?? true
8bce55bf
JB
2034 }
2035
66a7748d 2036 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
28e78158 2037 if (this.hasEvses) {
3fa7f799
JB
2038 for (const [evseId, evseStatus] of this.evses) {
2039 if (evseId === 0) {
66a7748d 2040 continue
3fa7f799 2041 }
28e78158
JB
2042 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2043 if (connectorStatus.transactionStarted === true) {
66a7748d 2044 await this.stopTransactionOnConnector(connectorId, reason)
28e78158
JB
2045 }
2046 }
2047 }
2048 } else {
2049 for (const connectorId of this.connectors.keys()) {
2050 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
66a7748d 2051 await this.stopTransactionOnConnector(connectorId, reason)
28e78158 2052 }
60ddad53
JB
2053 }
2054 }
2055 }
2056
1f761b9a 2057 // 0 for disabling
66a7748d 2058 private getConnectionTimeout (): number {
a807045b 2059 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
4e3b1d6b 2060 return convertToInt(
66a7748d 2061 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4e3b1d6b 2062 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value! ??
66a7748d
JB
2063 Constants.DEFAULT_CONNECTION_TIMEOUT
2064 )
291cb255 2065 }
66a7748d 2066 return Constants.DEFAULT_CONNECTION_TIMEOUT
3574dfd3
JB
2067 }
2068
66a7748d
JB
2069 private getPowerDivider (): number {
2070 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
be1e907c 2071 if (this.stationInfo?.powerSharedByConnectors === true) {
66a7748d 2072 powerDivider = this.getNumberOfRunningTransactions()
6ecb15e4 2073 }
66a7748d 2074 return powerDivider
6ecb15e4
JB
2075 }
2076
66a7748d
JB
2077 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2078 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2079 const maximumPower = (stationInfo ?? this.stationInfo).maximumPower!
fa7bccf4 2080 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
2081 case CurrentType.AC:
2082 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 2083 this.getNumberOfPhases(stationInfo),
b1bbdae5 2084 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
66a7748d
JB
2085 this.getVoltageOut(stationInfo)
2086 )
cc6e8ab5 2087 case CurrentType.DC:
66a7748d 2088 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
cc6e8ab5
JB
2089 }
2090 }
2091
66a7748d
JB
2092 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2093 return (stationInfo ?? this.stationInfo).currentOutType ?? CurrentType.AC
5398cecf
JB
2094 }
2095
66a7748d 2096 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
74ed61d9
JB
2097 return (
2098 (stationInfo ?? this.stationInfo).voltageOut ??
2099 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
66a7748d 2100 )
5398cecf
JB
2101 }
2102
66a7748d 2103 private getAmperageLimitation (): number | undefined {
cc6e8ab5 2104 if (
9bf0ef23 2105 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
66a7748d 2106 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
a807045b 2107 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) != null
cc6e8ab5
JB
2108 ) {
2109 return (
9bf0ef23 2110 convertToInt(
66a7748d
JB
2111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2112 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!)?.value
fba11dc6 2113 ) / getAmperageLimitationUnitDivider(this.stationInfo)
66a7748d 2114 )
cc6e8ab5
JB
2115 }
2116 }
2117
66a7748d 2118 private async startMessageSequence (): Promise<void> {
b7f9e41d 2119 if (this.stationInfo?.autoRegister === true) {
f7f98c68 2120 await this.ocppRequestService.requestHandler<
66a7748d
JB
2121 BootNotificationRequest,
2122 BootNotificationResponse
8bfbc743 2123 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
66a7748d
JB
2124 skipBufferingOnError: true
2125 })
6114e6f1 2126 }
136c90ba 2127 // Start WebSocket ping
66a7748d 2128 this.startWebSocketPing()
5ad8570f 2129 // Start heartbeat
66a7748d 2130 this.startHeartbeat()
0a60c33c 2131 // Initialize connectors status
c3b83130
JB
2132 if (this.hasEvses) {
2133 for (const [evseId, evseStatus] of this.evses) {
4334db72
JB
2134 if (evseId > 0) {
2135 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
66a7748d
JB
2136 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2137 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
4334db72 2138 }
c3b83130 2139 }
4334db72
JB
2140 }
2141 } else {
2142 for (const connectorId of this.connectors.keys()) {
2143 if (connectorId > 0) {
fba11dc6 2144 const connectorBootStatus = getBootConnectorStatus(
c3b83130
JB
2145 this,
2146 connectorId,
66a7748d
JB
2147 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2148 this.getConnectorStatus(connectorId)!
2149 )
2150 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
c3b83130
JB
2151 }
2152 }
5ad8570f 2153 }
9fe79a13 2154 if (this.stationInfo.firmwareStatus === FirmwareStatus.Installing) {
c9a4f9ea 2155 await this.ocppRequestService.requestHandler<
66a7748d
JB
2156 FirmwareStatusNotificationRequest,
2157 FirmwareStatusNotificationResponse
c9a4f9ea 2158 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
66a7748d
JB
2159 status: FirmwareStatus.Installed
2160 })
2161 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
c9a4f9ea 2162 }
3637ca2c 2163
0a60c33c 2164 // Start the ATG
66a7748d
JB
2165 if (this.getAutomaticTransactionGeneratorConfiguration().enable) {
2166 this.startAutomaticTransactionGenerator()
fa7bccf4 2167 }
66a7748d 2168 this.flushMessageBuffer()
fa7bccf4
JB
2169 }
2170
66a7748d 2171 private async stopMessageSequence (
9ff486f4 2172 reason?: StopTransactionReason,
66a7748d 2173 stopTransactions = this.stationInfo?.stopTransactionsOnStopped
e7aeea18 2174 ): Promise<void> {
136c90ba 2175 // Stop WebSocket ping
66a7748d 2176 this.stopWebSocketPing()
79411696 2177 // Stop heartbeat
66a7748d 2178 this.stopHeartbeat()
9ff486f4 2179 // Stop the ATG
b20eb107 2180 if (this.automaticTransactionGenerator?.started === true) {
66a7748d 2181 this.stopAutomaticTransactionGenerator()
79411696 2182 }
3e888c65 2183 // Stop ongoing transactions
66a7748d 2184 stopTransactions === true && (await this.stopRunningTransactions(reason))
039211f9
JB
2185 if (this.hasEvses) {
2186 for (const [evseId, evseStatus] of this.evses) {
2187 if (evseId > 0) {
2188 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2189 await this.ocppRequestService.requestHandler<
66a7748d
JB
2190 StatusNotificationRequest,
2191 StatusNotificationResponse
039211f9
JB
2192 >(
2193 this,
2194 RequestCommand.STATUS_NOTIFICATION,
041365be 2195 buildStatusNotificationRequest(
039211f9
JB
2196 this,
2197 connectorId,
12f26d4a 2198 ConnectorStatusEnum.Unavailable,
66a7748d
JB
2199 evseId
2200 )
2201 )
2202 delete connectorStatus?.status
039211f9
JB
2203 }
2204 }
2205 }
2206 } else {
2207 for (const connectorId of this.connectors.keys()) {
2208 if (connectorId > 0) {
2209 await this.ocppRequestService.requestHandler<
66a7748d
JB
2210 StatusNotificationRequest,
2211 StatusNotificationResponse
039211f9 2212 >(
6e939d9e 2213 this,
039211f9 2214 RequestCommand.STATUS_NOTIFICATION,
66a7748d
JB
2215 buildStatusNotificationRequest(this, connectorId, ConnectorStatusEnum.Unavailable)
2216 )
2217 delete this.getConnectorStatus(connectorId)?.status
039211f9 2218 }
45c0ae82
JB
2219 }
2220 }
79411696
JB
2221 }
2222
66a7748d 2223 private startWebSocketPing (): void {
4e3b1d6b 2224 const webSocketPingInterval: number =
a807045b 2225 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
4e3b1d6b 2226 ? convertToInt(
66a7748d
JB
2227 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2228 )
2229 : 0
a807045b 2230 if (webSocketPingInterval > 0 && this.webSocketPingSetInterval == null) {
ad2f27c3 2231 this.webSocketPingSetInterval = setInterval(() => {
66a7748d
JB
2232 if (this.isWebSocketConnectionOpened()) {
2233 this.wsConnection?.ping()
136c90ba 2234 }
66a7748d 2235 }, secondsToMilliseconds(webSocketPingInterval))
e7aeea18 2236 logger.info(
9bf0ef23 2237 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
66a7748d
JB
2238 webSocketPingInterval
2239 )}`
2240 )
a807045b 2241 } else if (this.webSocketPingSetInterval != null) {
e7aeea18 2242 logger.info(
9bf0ef23 2243 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
66a7748d
JB
2244 webSocketPingInterval
2245 )}`
2246 )
136c90ba 2247 } else {
e7aeea18 2248 logger.error(
66a7748d
JB
2249 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2250 )
136c90ba
JB
2251 }
2252 }
2253
66a7748d 2254 private stopWebSocketPing (): void {
a807045b 2255 if (this.webSocketPingSetInterval != null) {
66a7748d
JB
2256 clearInterval(this.webSocketPingSetInterval)
2257 delete this.webSocketPingSetInterval
136c90ba
JB
2258 }
2259 }
2260
66a7748d
JB
2261 private getConfiguredSupervisionUrl (): URL {
2262 let configuredSupervisionUrl: string
2263 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
9bf0ef23 2264 if (isNotEmptyArray(supervisionUrls)) {
66a7748d 2265 let configuredSupervisionUrlIndex: number
2dcfe98e 2266 switch (Configuration.getSupervisionUrlDistribution()) {
2dcfe98e 2267 case SupervisionUrlDistribution.RANDOM:
e1d9a0f4 2268 configuredSupervisionUrlIndex = Math.floor(
66a7748d
JB
2269 secureRandom() * (supervisionUrls as string[]).length
2270 )
2271 break
a52a6446 2272 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634 2273 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2dcfe98e 2274 default:
66a7748d
JB
2275 !Object.values(SupervisionUrlDistribution).includes(
2276 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2277 Configuration.getSupervisionUrlDistribution()!
2278 ) &&
a52a6446 2279 logger.error(
e1d9a0f4 2280 // eslint-disable-next-line @typescript-eslint/no-base-to-string
a52a6446
JB
2281 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2282 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
66a7748d
JB
2283 }`
2284 )
2285 configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length
2286 break
c0560973 2287 }
66a7748d 2288 configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex]
d5c3df49 2289 } else {
66a7748d 2290 configuredSupervisionUrl = supervisionUrls as string
d5c3df49 2291 }
9bf0ef23 2292 if (isNotEmptyString(configuredSupervisionUrl)) {
66a7748d 2293 return new URL(configuredSupervisionUrl)
c0560973 2294 }
66a7748d
JB
2295 const errorMsg = 'No supervision url(s) configured'
2296 logger.error(`${this.logPrefix()} ${errorMsg}`)
2297 throw new BaseError(`${errorMsg}`)
136c90ba
JB
2298 }
2299
66a7748d 2300 private stopHeartbeat (): void {
a807045b 2301 if (this.heartbeatSetInterval != null) {
66a7748d
JB
2302 clearInterval(this.heartbeatSetInterval)
2303 delete this.heartbeatSetInterval
7dde0b73 2304 }
5ad8570f
JB
2305 }
2306
66a7748d
JB
2307 private terminateWSConnection (): void {
2308 if (this.isWebSocketConnectionOpened()) {
2309 this.wsConnection?.terminate()
2310 this.wsConnection = null
55516218
JB
2311 }
2312 }
2313
66a7748d 2314 private async reconnect (): Promise<void> {
7874b0b1 2315 // Stop WebSocket ping
66a7748d 2316 this.stopWebSocketPing()
136c90ba 2317 // Stop heartbeat
66a7748d 2318 this.stopHeartbeat()
5ad8570f 2319 // Stop the ATG if needed
66a7748d
JB
2320 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure) {
2321 this.stopAutomaticTransactionGenerator()
ad2f27c3 2322 }
e7aeea18 2323 if (
66a7748d 2324 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5398cecf
JB
2325 this.autoReconnectRetryCount < this.stationInfo.autoReconnectMaxRetries! ||
2326 this.stationInfo?.autoReconnectMaxRetries === -1
e7aeea18 2327 ) {
66a7748d 2328 ++this.autoReconnectRetryCount
5398cecf
JB
2329 const reconnectDelay =
2330 this.stationInfo?.reconnectExponentialDelay === true
2331 ? exponentialDelay(this.autoReconnectRetryCount)
66a7748d
JB
2332 : secondsToMilliseconds(this.getConnectionTimeout())
2333 const reconnectDelayWithdraw = 1000
1e080116 2334 const reconnectTimeout =
66a7748d 2335 reconnectDelay != null && reconnectDelay - reconnectDelayWithdraw > 0
1e080116 2336 ? reconnectDelay - reconnectDelayWithdraw
66a7748d 2337 : 0
e7aeea18 2338 logger.error(
9bf0ef23 2339 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
e7aeea18 2340 reconnectDelay,
66a7748d
JB
2341 2
2342 )}ms, timeout ${reconnectTimeout}ms`
2343 )
2344 await sleep(reconnectDelay)
e7aeea18 2345 logger.error(
66a7748d
JB
2346 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2347 )
e7aeea18 2348 this.openWSConnection(
59b6ed8d 2349 {
66a7748d 2350 handshakeTimeout: reconnectTimeout
59b6ed8d 2351 },
66a7748d
JB
2352 { closeOpened: true }
2353 )
5398cecf 2354 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
e7aeea18 2355 logger.error(
d56ea27c 2356 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2357 this.autoReconnectRetryCount
66a7748d
JB
2358 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2359 )
5ad8570f
JB
2360 }
2361 }
7dde0b73 2362}