import { createHash, randomBytes } from 'node:crypto'
import type { EventEmitter } from 'node:events'
-import { basename, dirname, join } from 'node:path'
+import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
import { env } from 'node:process'
import { fileURLToPath } from 'node:url'
import chalk from 'chalk'
import {
- type Interval,
addDays,
addSeconds,
addWeeks,
differenceInDays,
differenceInSeconds,
differenceInWeeks,
+ type Interval,
isAfter,
isBefore,
isDate,
toDate
} from 'date-fns'
import { maxTime } from 'date-fns/constants'
+import { isEmpty } from 'rambda'
-import type { ChargingStation } from './ChargingStation.js'
-import { getConfigurationKey } from './ConfigurationKeyUtils.js'
import { BaseError } from '../exception/index.js'
import {
AmpereUnits,
BootReasonEnumType,
type ChargingProfile,
ChargingProfileKindType,
+ ChargingProfilePurposeType,
ChargingRateUnitType,
type ChargingSchedulePeriod,
type ChargingStationConfiguration,
type ChargingStationInfo,
+ type ChargingStationOptions,
type ChargingStationTemplate,
type ChargingStationWorkerMessageEvents,
ConnectorPhaseRotation,
} from '../types/index.js'
import {
ACElectricUtils,
+ clone,
Constants,
- DCElectricUtils,
- cloneObject,
convertToDate,
convertToInt,
+ DCElectricUtils,
isArraySorted,
- isEmptyObject,
- isEmptyString,
isNotEmptyArray,
isNotEmptyString,
- isValidTime,
+ isValidDate,
logger,
secureRandom
} from '../utils/index.js'
+import type { ChargingStation } from './ChargingStation.js'
+import { getConfigurationKey } from './ConfigurationKeyUtils.js'
const moduleName = 'Helpers'
+export const buildTemplateName = (templateFile: string): string => {
+ if (isAbsolute(templateFile)) {
+ templateFile = relative(
+ resolve(join(dirname(fileURLToPath(import.meta.url)), 'assets', 'station-templates')),
+ templateFile
+ )
+ }
+ const templateFileParsedPath = parse(templateFile)
+ return join(templateFileParsedPath.dir, templateFileParsedPath.name)
+}
+
export const getChargingStationId = (
index: number,
stationTemplate: ChargingStationTemplate | undefined
const chargingStationInfo = {
chargePointModel: stationTemplate.chargePointModel,
chargePointVendor: stationTemplate.chargePointVendor,
- ...(stationTemplate.chargeBoxSerialNumberPrefix !== undefined && {
+ ...(stationTemplate.chargeBoxSerialNumberPrefix != null && {
chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
}),
- ...(stationTemplate.chargePointSerialNumberPrefix !== undefined && {
+ ...(stationTemplate.chargePointSerialNumberPrefix != null && {
chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
}),
- ...(stationTemplate.meterSerialNumberPrefix !== undefined && {
+ ...(stationTemplate.meterSerialNumberPrefix != null && {
meterSerialNumber: stationTemplate.meterSerialNumberPrefix
}),
- ...(stationTemplate.meterType !== undefined && {
+ ...(stationTemplate.meterType != null && {
meterType: stationTemplate.meterType
})
}
logger.error(`${logPrefix} ${errorMsg}`)
throw new BaseError(errorMsg)
}
- if (isEmptyObject(stationTemplate)) {
+ if (isEmpty(stationTemplate)) {
const errorMsg = `Empty charging station information from template file ${templateFile}`
logger.error(`${logPrefix} ${errorMsg}`)
throw new BaseError(errorMsg)
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
- stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION
- logger.warn(
- `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
- Constants.DEFAULT_ATG_CONFIGURATION
- )
- }
- if (stationTemplate.idTagsFile == null || isEmptyString(stationTemplate.idTagsFile)) {
+ if (stationTemplate.idTagsFile == null || isEmpty(stationTemplate.idTagsFile)) {
logger.warn(
`${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
)
logger.error(`${logPrefix} ${errorMsg}`)
throw new BaseError(errorMsg)
}
- if (isEmptyObject(stationConfiguration)) {
+ if (isEmpty(stationConfiguration)) {
const errorMsg = `Empty charging station configuration from file ${configurationFile}`
logger.error(`${logPrefix} ${errorMsg}`)
throw new BaseError(errorMsg)
)
stationTemplate.randomConnectors = true
}
- return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }
+ return {
+ configuredMaxConnectors,
+ templateMaxConnectors,
+ templateMaxAvailableConnectors
+ }
}
export const checkStationInfoConnectorStatus = (
}
}
+export const setChargingStationOptions = (
+ stationInfo: ChargingStationInfo,
+ options?: ChargingStationOptions
+): ChargingStationInfo => {
+ if (options?.supervisionUrls != null) {
+ stationInfo.supervisionUrls = options.supervisionUrls
+ }
+ if (options?.persistentConfiguration != null) {
+ stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
+ stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
+ stationInfo.automaticTransactionGeneratorPersistentConfiguration =
+ options.persistentConfiguration
+ }
+ if (options?.autoStart != null) {
+ stationInfo.autoStart = options.autoStart
+ }
+ if (options?.autoRegister != null) {
+ stationInfo.autoRegister = options.autoRegister
+ }
+ if (options?.enableStatistics != null) {
+ stationInfo.enableStatistics = options.enableStatistics
+ }
+ if (options?.ocppStrictCompliance != null) {
+ stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
+ }
+ if (options?.stopTransactionsOnStopped != null) {
+ stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
+ }
+ return stationInfo
+}
+
export const buildConnectorsMap = (
connectors: Record<string, ConnectorStatus>,
logPrefix: string,
const connectorStatus = connectors[connector]
const connectorId = convertToInt(connector)
checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
- connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
+ connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
}
} else {
logger.warn(
for (const connectorId of connectors.keys()) {
if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
logger.warn(
- `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
- connectorId
- )?.transactionId}`
+ `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
+ connectors.get(connectorId)?.transactionId
+ }`
)
}
if (connectorId === 0) {
}
}
-export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
- connectorStatus.chargingProfiles =
- connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
- ? connectorStatus.chargingProfiles?.filter(
- (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId
- )
- : []
+export const resetAuthorizeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
connectorStatus.idTagLocalAuthorized = false
connectorStatus.idTagAuthorized = false
+ delete connectorStatus.localAuthorizeIdTag
+ delete connectorStatus.authorizeIdTag
+}
+
+export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
+ if (connectorStatus == null) {
+ return
+ }
+ if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
+ connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.filter(
+ chargingProfile =>
+ (chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
+ chargingProfile.transactionId != null &&
+ connectorStatus.transactionId != null &&
+ chargingProfile.transactionId !== connectorStatus.transactionId) ||
+ chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE
+ )
+ }
+ resetAuthorizeConnectorStatus(connectorStatus)
connectorStatus.transactionRemoteStarted = false
connectorStatus.transactionStarted = false
delete connectorStatus.transactionStart
delete connectorStatus.transactionId
- delete connectorStatus.localAuthorizeIdTag
- delete connectorStatus.authorizeIdTag
delete connectorStatus.transactionIdTag
connectorStatus.transactionEnergyActiveImportRegisterValue = 0
delete connectorStatus.transactionBeginMeterValue
}
+export const prepareConnectorStatus = (connectorStatus: ConnectorStatus): ConnectorStatus => {
+ if (connectorStatus.reservation != null) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)!
+ }
+ if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
+ connectorStatus.chargingProfiles = connectorStatus.chargingProfiles
+ .filter(
+ chargingProfile =>
+ chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE
+ )
+ .map(chargingProfile => {
+ chargingProfile.chargingSchedule.startSchedule = convertToDate(
+ chargingProfile.chargingSchedule.startSchedule
+ )
+ chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
+ chargingProfile.validTo = convertToDate(chargingProfile.validTo)
+ return chargingProfile
+ })
+ }
+ return connectorStatus
+}
+
export const createBootNotificationRequest = (
stationInfo: ChargingStationInfo,
bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
return {
chargePointModel: stationInfo.chargePointModel,
chargePointVendor: stationInfo.chargePointVendor,
- ...(stationInfo.chargeBoxSerialNumber !== undefined && {
+ ...(stationInfo.chargeBoxSerialNumber != null && {
chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
}),
- ...(stationInfo.chargePointSerialNumber !== undefined && {
+ ...(stationInfo.chargePointSerialNumber != null && {
chargePointSerialNumber: stationInfo.chargePointSerialNumber
}),
- ...(stationInfo.firmwareVersion !== undefined && {
+ ...(stationInfo.firmwareVersion != null && {
firmwareVersion: stationInfo.firmwareVersion
}),
- ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
- ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi }),
- ...(stationInfo.meterSerialNumber !== undefined && {
+ ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
+ ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
+ ...(stationInfo.meterSerialNumber != null && {
meterSerialNumber: stationInfo.meterSerialNumber
}),
- ...(stationInfo.meterType !== undefined && {
+ ...(stationInfo.meterType != null && {
meterType: stationInfo.meterType
})
} satisfies OCPP16BootNotificationRequest
chargingStation: {
model: stationInfo.chargePointModel,
vendorName: stationInfo.chargePointVendor,
- ...(stationInfo.firmwareVersion !== undefined && {
+ ...(stationInfo.firmwareVersion != null && {
firmwareVersion: stationInfo.firmwareVersion
}),
- ...(stationInfo.chargeBoxSerialNumber !== undefined && {
+ ...(stationInfo.chargeBoxSerialNumber != null && {
serialNumber: stationInfo.chargeBoxSerialNumber
}),
- ...((stationInfo.iccid !== undefined || stationInfo.imsi !== undefined) && {
+ ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
modem: {
- ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
- ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi })
+ ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
+ ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
}
})
}
templateKey.deprecatedKey,
logPrefix,
templateFile,
- templateKey.key !== undefined ? `Use '${templateKey.key}' instead` : undefined
+ templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
)
convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
}
export const stationTemplateToStationInfo = (
stationTemplate: ChargingStationTemplate
): ChargingStationInfo => {
- stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate)
+ stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
delete stationTemplate.power
delete stationTemplate.powerUnit
delete stationTemplate.Connectors
delete stationTemplate.Evses
delete stationTemplate.Configuration
delete stationTemplate.AutomaticTransactionGenerator
+ delete stationTemplate.numberOfConnectors
delete stationTemplate.chargeBoxSerialNumberPrefix
delete stationTemplate.chargePointSerialNumberPrefix
delete stationTemplate.meterSerialNumberPrefix
randomSerialNumber?: boolean
}
): void => {
- params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
+ params = {
+ ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
+ ...params
+ }
const serialNumberSuffix =
params.randomSerialNumber === true
? getRandomSerialNumberSuffix({
}
/**
- * Gets the connector cloned charging profiles applying a power limitation
- * and sorted by connector id descending then stack level descending
+ * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
*
- * @param chargingStation -
- * @param connectorId -
+ * @param chargingStation - Charging station
+ * @param connectorId - Connector id
* @returns connector charging profiles array
*/
export const getConnectorChargingProfiles = (
chargingStation: ChargingStation,
connectorId: number
): ChargingProfile[] => {
- return cloneObject<ChargingProfile[]>(
- (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
- .sort((a, b) => b.stackLevel - a.stackLevel)
- .concat(
- (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
- (a, b) => b.stackLevel - a.stackLevel
+ // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
+ return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
+ .slice()
+ .sort((a, b) => {
+ if (
+ a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
+ b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
+ ) {
+ return -1
+ } else if (
+ a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
+ b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
+ ) {
+ return 1
+ }
+ return b.stackLevel - a.stackLevel
+ })
+ .concat(
+ (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
+ .filter(
+ chargingProfile =>
+ chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
)
- )
- )
+ .sort((a, b) => b.stackLevel - a.stackLevel)
+ )
}
export const getChargingStationConnectorChargingProfilesPowerLimit = (
connectorId: number
): number | undefined => {
let limit: number | undefined, chargingProfile: ChargingProfile | undefined
- // Get charging profiles sorted by connector id then stack level
const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
if (isNotEmptyArray(chargingProfiles)) {
const result = getLimitFromChargingProfiles(
event: ChargingStationWorkerMessageEvents,
eventsToWait: number
): Promise<number> => {
- return await new Promise<number>((resolve) => {
+ return await new Promise<number>(resolve => {
let events = 0
if (eventsToWait === 0) {
resolve(events)
const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
let configuredMaxNumberOfConnectors = 0
if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
- const numberOfConnectors = stationTemplate.numberOfConnectors as number[]
+ const numberOfConnectors = stationTemplate.numberOfConnectors
configuredMaxNumberOfConnectors =
numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
} else if (stationTemplate.numberOfConnectors != null) {
- configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number
+ configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
} else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
configuredMaxNumberOfConnectors =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
templateFile: string,
logMsgToAppend = ''
): void => {
- if (template[key as keyof ChargingStationTemplate] !== undefined) {
+ if (template[key as keyof ChargingStationTemplate] != null) {
const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
}`
deprecatedKey: string,
key?: string
): void => {
- if (template[deprecatedKey as keyof ChargingStationTemplate] !== undefined) {
- if (key !== undefined) {
+ if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
+ if (key != null) {
(template as unknown as Record<string, unknown>)[key] =
template[deprecatedKey as keyof ChargingStationTemplate]
}
): ChargingProfilesLimit | undefined => {
const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
const currentDate = new Date()
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ let previousActiveChargingProfile: ChargingProfile | undefined
for (const chargingProfile of chargingProfiles) {
const chargingSchedule = chargingProfile.chargingSchedule
if (chargingSchedule.startSchedule == null) {
`${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
)
// OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
- chargingSchedule.startSchedule = connectorStatus.transactionStart
+ chargingSchedule.startSchedule = connectorStatus?.transactionStart
}
if (!isDate(chargingSchedule.startSchedule)) {
logger.warn(
) {
// Found the schedule period: previous is the correct one
const result: ChargingProfilesLimit = {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- limit: previousChargingSchedulePeriod!.limit,
- chargingProfile
+ limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
+ chargingProfile: previousActiveChargingProfile ?? chargingProfile
}
logger.debug(debugLogMsg, result)
return result
}
- // Keep a reference to previous one
- previousChargingSchedulePeriod = chargingSchedulePeriod
// Handle the last schedule period within the charging profile duration
if (
index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
) > chargingSchedule.duration)
) {
const result: ChargingProfilesLimit = {
- limit: previousChargingSchedulePeriod.limit,
+ limit: chargingSchedulePeriod.limit,
chargingProfile
}
logger.debug(debugLogMsg, result)
return result
}
+ // Keep a reference to previous charging schedule period
+ previousChargingSchedulePeriod = chargingSchedulePeriod
}
}
+ // Keep a reference to previous active charging profile
+ previousActiveChargingProfile = chargingProfile
}
}
}
export const prepareChargingProfileKind = (
- connectorStatus: ConnectorStatus,
+ connectorStatus: ConnectorStatus | undefined,
chargingProfile: ChargingProfile,
- currentDate: Date,
+ currentDate: string | number | Date,
logPrefix: string
): boolean => {
switch (chargingProfile.chargingProfileKind) {
)
delete chargingProfile.chargingSchedule.startSchedule
}
- if (connectorStatus.transactionStarted === true) {
+ if (connectorStatus?.transactionStarted === true) {
chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
}
- // FIXME: Handle relative charging profile duration
+ // FIXME: handle relative charging profile duration
break
}
return true
export const canProceedChargingProfile = (
chargingProfile: ChargingProfile,
- currentDate: Date,
+ currentDate: string | number | Date,
logPrefix: string
): boolean => {
if (
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
+ (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
+ (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
) {
logger.debug(
`${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
chargingProfile.chargingProfileId
- } is not valid for the current date ${currentDate.toISOString()}`
+ } is not valid for the current date ${
+ isDate(currentDate) ? currentDate.toISOString() : currentDate
+ }`
)
return false
}
)
return false
}
- if (!isValidTime(chargingProfile.chargingSchedule.startSchedule)) {
+ if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
logger.error(
`${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
)
*/
const prepareRecurringChargingProfile = (
chargingProfile: ChargingProfile,
- currentDate: Date,
+ currentDate: string | number | Date,
logPrefix: string
): boolean => {
const chargingSchedule = chargingProfile.chargingSchedule
let recurringIntervalTranslated = false
- let recurringInterval: Interval
+ let recurringInterval: Interval | undefined
switch (chargingProfile.recurrencyKind) {
case RecurrencyKindType.DAILY:
recurringInterval = {
`${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
chargingProfile.recurrencyKind
} charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- recurringInterval!.start
+ recurringInterval?.start as Date
).toISOString()}, ${toDate(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- recurringInterval!.end
- ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
+ recurringInterval?.end as Date
+ ).toISOString()}] has not been properly translated to current date ${
+ isDate(currentDate) ? currentDate.toISOString() : currentDate
+ } `
)
}
return recurringIntervalTranslated