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,
} from '../types/index.js'
import {
ACElectricUtils,
- Constants,
- DCElectricUtils,
clone,
+ Constants,
convertToDate,
convertToInt,
+ DCElectricUtils,
isArraySorted,
- isEmptyObject,
- isEmptyString,
isNotEmptyArray,
isNotEmptyString,
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
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)
}
- 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 buildConnectorsMap = (
- connectors: Record<string, ConnectorStatus>,
- logPrefix: string,
- templateFile: string
-): Map<number, ConnectorStatus> => {
- const connectorsMap = new Map<number, ConnectorStatus>()
- if (getMaxNumberOfConnectors(connectors) > 0) {
- for (const connector in connectors) {
- const connectorStatus = connectors[connector]
- const connectorId = convertToInt(connector)
- checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
- connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
- }
- } else {
- logger.warn(
- `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
- )
- }
- return connectorsMap
-}
-
export const setChargingStationOptions = (
- chargingStation: ChargingStation,
stationInfo: ChargingStationInfo,
options?: ChargingStationOptions
): ChargingStationInfo => {
if (options?.supervisionUrls != null) {
- chargingStation.setSupervisionUrls(options.supervisionUrls, false)
+ stationInfo.supervisionUrls = options.supervisionUrls
}
if (options?.persistentConfiguration != null) {
stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
return stationInfo
}
+export const buildConnectorsMap = (
+ connectors: Record<string, ConnectorStatus>,
+ logPrefix: string,
+ templateFile: string
+): Map<number, ConnectorStatus> => {
+ const connectorsMap = new Map<number, ConnectorStatus>()
+ if (getMaxNumberOfConnectors(connectors) > 0) {
+ for (const connector in connectors) {
+ const connectorStatus = connectors[connector]
+ const connectorId = convertToInt(connector)
+ checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
+ connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
+ }
+ } else {
+ logger.warn(
+ `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
+ )
+ }
+ return connectorsMap
+}
+
export const initializeConnectorsMapStatus = (
connectors: Map<number, ConnectorStatus>,
logPrefix: string
}
}
+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
}
- connectorStatus.chargingProfiles =
- connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
- ? connectorStatus.chargingProfiles.filter(
- chargingProfile => chargingProfile.transactionId !== connectorStatus.transactionId
- )
- : []
- connectorStatus.idTagLocalAuthorized = false
- connectorStatus.idTagAuthorized = false
+ 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
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 clone<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(
const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
const currentDate = new Date()
const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ let previousActiveChargingProfile: ChargingProfile | undefined
for (const chargingProfile of chargingProfiles) {
const chargingSchedule = chargingProfile.chargingSchedule
if (chargingSchedule.startSchedule == null) {
) {
// 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
}
}
}
if (connectorStatus?.transactionStarted === true) {
chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
}
- // FIXME: Handle relative charging profile duration
+ // FIXME: handle relative charging profile duration
break
}
return true