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