import { createHash } from 'node:crypto'
import { EventEmitter } from 'node:events'
-import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { dirname, join } from 'node:path'
+import { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { dirname, join, parse } from 'node:path'
import { URL } from 'node:url'
import { parentPort } from 'node:worker_threads'
import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
-import merge from 'just-merge'
+import { mergeDeepRight } from 'rambda'
import { type RawData, WebSocket } from 'ws'
import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
hasReservationExpired,
initializeConnectorsMapStatus,
propagateSerialNumber,
+ setChargingStationOptions,
stationTemplateToStationInfo,
warnTemplateKeysDeprecation
} from './Helpers.js'
ChargingStationEvents,
type ChargingStationInfo,
type ChargingStationOcppConfiguration,
+ type ChargingStationOptions,
type ChargingStationTemplate,
type ConnectorStatus,
ConnectorStatusEnum,
FirmwareStatus,
type FirmwareStatusNotificationRequest,
type FirmwareStatusNotificationResponse,
- type FirmwareUpgrade,
type HeartbeatRequest,
type HeartbeatResponse,
type IncomingRequest,
Configuration,
Constants,
DCElectricUtils,
+ buildAddedMessage,
buildChargingStationAutomaticTransactionGeneratorConfiguration,
buildConnectorsStatus,
+ buildDeletedMessage,
buildEvsesStatus,
buildStartedMessage,
buildStoppedMessage,
public started: boolean
public starting: boolean
public idTagsCache: IdTagsCache
- public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined
- public ocppConfiguration!: ChargingStationOcppConfiguration | undefined
+ public automaticTransactionGenerator?: AutomaticTransactionGenerator
+ public ocppConfiguration?: ChargingStationOcppConfiguration
public wsConnection: WebSocket | null
public readonly connectors: Map<number, ConnectorStatus>
public readonly evses: Map<number, EvseStatus>
public readonly requests: Map<string, CachedRequest>
- public performanceStatistics!: PerformanceStatistics | undefined
+ public performanceStatistics?: PerformanceStatistics
public heartbeatSetInterval?: NodeJS.Timeout
public ocppRequestService!: OCPPRequestService
public bootNotificationRequest?: BootNotificationRequest
private configuredSupervisionUrl!: URL
private wsConnectionRetried: boolean
private wsConnectionRetryCount: number
- private templateFileWatcher!: FSWatcher | undefined
+ private templateFileWatcher?: FSWatcher
private templateFileHash!: string
private readonly sharedLRUCache: SharedLRUCache
private wsPingSetInterval?: NodeJS.Timeout
private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
private flushMessageBufferSetInterval?: NodeJS.Timeout
- constructor (index: number, templateFile: string) {
+ constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
super()
this.started = false
this.starting = false
this.idTagsCache = IdTagsCache.getInstance()
this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
+ this.on(ChargingStationEvents.added, () => {
+ parentPort?.postMessage(buildAddedMessage(this))
+ })
+ this.on(ChargingStationEvents.deleted, () => {
+ parentPort?.postMessage(buildDeletedMessage(this))
+ })
this.on(ChargingStationEvents.started, () => {
parentPort?.postMessage(buildStartedMessage(this))
})
})
this.wsConnectionRetried = false
})
+ this.on(ChargingStationEvents.rejected, () => {
+ this.wsConnectionRetried = false
+ })
this.on(ChargingStationEvents.disconnected, () => {
try {
this.internalStopMessageSequence()
}
})
- this.initialize()
+ this.initialize(options)
+
+ this.add()
+
+ if (this.stationInfo?.autoStart === true) {
+ this.start()
+ }
}
public get hasEvses (): boolean {
return this.connectors.size === 0 && this.evses.size > 0
}
- private get wsConnectionUrl (): URL {
+ public get wsConnectionUrl (): URL {
return new URL(
`${
this.stationInfo?.supervisionUrlOcppConfiguration === true &&
return Constants.DEFAULT_HEARTBEAT_INTERVAL
}
+ public setSupervisionUrls (urls: string | string[], saveStationInfo = true): void {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.stationInfo!.supervisionUrls = urls
+ if (saveStationInfo) {
+ this.saveStationInfo()
+ }
+ this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+ }
+
public setSupervisionUrl (url: string): void {
if (
this.stationInfo?.supervisionUrlOcppConfiguration === true &&
) {
setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
} else {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.stationInfo!.supervisionUrls = url
- this.saveStationInfo()
- this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+ this.setSupervisionUrls(url)
}
}
}
}
+ private add (): void {
+ this.emit(ChargingStationEvents.added)
+ }
+
+ public async delete (deleteConfiguration = true): Promise<void> {
+ if (this.started) {
+ await this.stop()
+ }
+ AutomaticTransactionGenerator.deleteInstance(this)
+ PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+ this.requests.clear()
+ this.connectors.clear()
+ this.evses.clear()
+ this.templateFileWatcher?.unref()
+ deleteConfiguration && rmSync(this.configurationFile, { force: true })
+ this.chargingStationWorkerBroadcastChannel.unref()
+ this.emit(ChargingStationEvents.deleted)
+ this.removeAllListeners()
+ }
+
public start (): void {
if (!this.started) {
if (!this.starting) {
} file have changed, reload`
)
this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
- // Initialize
- this.initialize()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+ // Initialize
+ this.initialize()
// Restart the ATG
const ATGStarted = this.automaticTransactionGenerator?.started
if (ATGStarted === true) {
}
}
- public async stop (reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
+ public async stop (
+ reason?: StopTransactionReason,
+ stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+ ): Promise<void> {
if (this.started) {
if (!this.stopping) {
this.stopping = true
if (this.stationInfo?.enableStatistics === true) {
this.performanceStatistics?.stop()
}
- this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
this.templateFileWatcher?.close()
- this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
delete this.bootNotificationResponse
this.started = false
this.saveConfiguration()
+ this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
this.emit(ChargingStationEvents.stopped)
this.stopping = false
} else {
if (this.isWebSocketConnectionOpened()) {
logger.warn(
- `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
+ `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
)
return
}
- logger.info(
- `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
- )
+ logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
this.wsConnection = new WebSocket(
this.wsConnectionUrl,
connectorId?: number
): boolean {
const reservation = this.getReservationBy('reservationId', reservationId)
- const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
+ const reservationExists = reservation != null && !hasReservationExpired(reservation)
if (arguments.length === 1) {
return !reservationExists
} else if (arguments.length > 1) {
- const userReservation =
- idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
+ const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined
const userReservationExists =
- userReservation !== undefined && !hasReservationExpired(userReservation)
- const notConnectorZero = connectorId === undefined ? true : connectorId > 0
+ userReservation != null && !hasReservationExpired(userReservation)
+ const notConnectorZero = connectorId == null ? true : connectorId > 0
const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
return (
!reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
}
const stationInfo = stationTemplateToStationInfo(stationTemplate)
stationInfo.hashId = getHashId(this.index, stationTemplate)
+ stationInfo.templateIndex = this.index
+ stationInfo.templateName = parse(this.templateFile).name
stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
- stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
createSerialNumber(stationTemplate, stationInfo)
stationInfo.voltageOut = this.getVoltageOut(stationInfo)
if (isNotEmptyArray(stationTemplate.power)) {
: stationTemplate.power
}
stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
- stationInfo.firmwareVersionPattern =
- stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
if (
+ isNotEmptyString(stationInfo.firmwareVersionPattern) &&
isNotEmptyString(stationInfo.firmwareVersion) &&
!new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
) {
} does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
)
}
- stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
+ stationInfo.firmwareUpgrade = mergeDeepRight(
{
versionUpgrade: {
step: 1
},
stationTemplate.firmwareUpgrade ?? {}
)
- stationInfo.resetTime =
- stationTemplate.resetTime != null
- ? secondsToMilliseconds(stationTemplate.resetTime)
- : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
+ if (stationTemplate.resetTime != null) {
+ stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
+ }
return stationInfo
}
private getStationInfoFromFile (
- stationInfoPersistentConfiguration = true
+ stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
+ .stationInfoPersistentConfiguration
): ChargingStationInfo | undefined {
let stationInfo: ChargingStationInfo | undefined
- if (stationInfoPersistentConfiguration) {
+ if (stationInfoPersistentConfiguration === true) {
stationInfo = this.getConfigurationFromFile()?.stationInfo
if (stationInfo != null) {
delete stationInfo.infoHash
+ delete (stationInfo as ChargingStationTemplate).numberOfConnectors
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (stationInfo.templateIndex == null) {
+ stationInfo.templateIndex = this.index
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (stationInfo.templateName == null) {
+ stationInfo.templateName = parse(this.templateFile).name
+ }
}
}
return stationInfo
}
- private getStationInfo (): ChargingStationInfo {
- const defaultStationInfo = Constants.DEFAULT_STATION_INFO
+ private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
const stationInfoFromTemplate = this.getStationInfoFromTemplate()
+ options?.persistentConfiguration != null &&
+ (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
const stationInfoFromFile = this.getStationInfoFromFile(
stationInfoFromTemplate.stationInfoPersistentConfiguration
)
stationInfoFromFile != null &&
stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
) {
- return { ...defaultStationInfo, ...stationInfoFromFile }
+ return setChargingStationOptions(
+ this,
+ { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile },
+ options
+ )
}
stationInfoFromFile != null &&
propagateSerialNumber(
stationInfoFromFile,
stationInfoFromTemplate
)
- return { ...defaultStationInfo, ...stationInfoFromTemplate }
+ return setChargingStationOptions(
+ this,
+ { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate },
+ options
+ )
}
private saveStationInfo (): void {
throw new BaseError(errorMsg)
}
- private initialize (): void {
+ private initialize (options?: ChargingStationOptions): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const stationTemplate = this.getTemplateFromFile()!
checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
} else {
this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
}
- this.stationInfo = this.getStationInfo()
+ this.stationInfo = this.getStationInfo(options)
if (
this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
- isNotEmptyString(this.stationInfo.firmwareVersion) &&
- isNotEmptyString(this.stationInfo.firmwareVersionPattern)
+ isNotEmptyString(this.stationInfo.firmwareVersionPattern) &&
+ isNotEmptyString(this.stationInfo.firmwareVersion)
) {
const patternGroup =
this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
this.bootNotificationRequest = bootNotificationRequest
this.powerDivider = this.getPowerDivider()
// OCPP configuration
- this.ocppConfiguration = this.getOcppConfiguration()
+ this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration)
this.initializeOcppConfiguration()
this.initializeOcppServices()
if (this.stationInfo.autoRegister === true) {
} else {
delete configurationData.configurationKey
}
- configurationData = merge<ChargingStationConfiguration>(
+ configurationData = mergeDeepRight(
configurationData,
buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
)
- if (
- this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
- this.getAutomaticTransactionGeneratorConfiguration() == null
- ) {
+ if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) {
delete configurationData.automaticTransactionGenerator
}
if (this.connectors.size > 0) {
return this.getTemplateFromFile()?.Configuration
}
- private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
+ private getOcppConfigurationFromFile (
+ ocppPersistentConfiguration?: boolean
+ ): ChargingStationOcppConfiguration | undefined {
const configurationKey = this.getConfigurationFromFile()?.configurationKey
- if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
+ if (ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
return { configurationKey }
}
return undefined
}
- private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
+ private getOcppConfiguration (
+ ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
+ ): ChargingStationOcppConfiguration | undefined {
let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
- this.getOcppConfigurationFromFile()
+ this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
if (ocppConfiguration == null) {
ocppConfiguration = this.getOcppConfigurationFromTemplate()
}
private async onOpen (): Promise<void> {
if (this.isWebSocketConnectionOpened()) {
+ this.emit(ChargingStationEvents.updated)
logger.info(
- `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
+ `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
)
let registrationRetryCount = 0
if (!this.isRegistered()) {
this.emit(ChargingStationEvents.updated)
} else {
logger.warn(
- `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
+ `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
)
}
}
private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
this.emit(ChargingStationEvents.disconnected)
+ this.emit(ChargingStationEvents.updated)
switch (code) {
// Normal close
case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
)}' and reason '${reason.toString()}'`
)
this.started &&
- this.reconnect().catch(error =>
- logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
- )
+ this.reconnect()
+ .then(() => {
+ this.emit(ChargingStationEvents.updated)
+ })
+ .catch(error => logger.error(`${this.logPrefix()} Error while reconnecting:`, error))
break
}
- this.emit(ChargingStationEvents.updated)
}
- private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
+ private getCachedRequest (
+ messageType: MessageType | undefined,
+ messageId: string
+ ): CachedRequest | undefined {
const cachedRequest = this.requests.get(messageId)
if (Array.isArray(cachedRequest)) {
return cachedRequest
if (!(error instanceof OCPPError)) {
logger.warn(
`${this.logPrefix()} Error thrown at incoming OCPP command '${
- commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
+ commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
// eslint-disable-next-line @typescript-eslint/no-base-to-string
}' message '${data.toString()}' handling is not an OCPPError:`,
error
}
logger.error(
`${this.logPrefix()} Incoming OCPP command '${
- commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
+ commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
// eslint-disable-next-line @typescript-eslint/no-base-to-string
}' message '${data.toString()}'${
- messageType !== MessageType.CALL_MESSAGE
- ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
+ this.requests.has(messageId)
+ ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
: ''
} processing error:`,
error
}
private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
- return stationTemplate?.useConnectorId0 ?? true
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
}
private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
}
private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return (stationInfo ?? this.stationInfo!).currentOutType ?? CurrentType.AC
+ return (
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (stationInfo ?? this.stationInfo!).currentOutType ??
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Constants.DEFAULT_STATION_INFO.currentOutType!
+ )
}
private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
private async stopMessageSequence (
reason?: StopTransactionReason,
- stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+ stopTransactions?: boolean
): Promise<void> {
this.internalStopMessageSequence()
// Stop ongoing transactions