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