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