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