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