fix: allow to set charging profile with TxProfile purpose
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
index 8870ce86ab3882a89e0c34364026495eaf115d84..2c264cd5612cfbeb8d1ccf01347e06810fcabf0d 100644 (file)
@@ -1,18 +1,18 @@
 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,
@@ -21,9 +21,8 @@ import {
   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,
@@ -32,6 +31,7 @@ import {
   BootReasonEnumType,
   type ChargingProfile,
   ChargingProfileKindType,
+  ChargingProfilePurposeType,
   ChargingRateUnitType,
   type ChargingSchedulePeriod,
   type ChargingStationConfiguration,
@@ -56,23 +56,34 @@ import {
 } 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
@@ -146,16 +157,16 @@ export const getHashId = (index: number, stationTemplate: ChargingStationTemplat
   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
     })
   }
@@ -243,12 +254,12 @@ export const checkTemplate = (
     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`
     )
@@ -265,7 +276,7 @@ export const checkConfiguration = (
     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)
@@ -296,7 +307,11 @@ export const checkConnectorsConfiguration = (
     )
     stationTemplate.randomConnectors = true
   }
-  return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }
+  return {
+    configuredMaxConnectors,
+    templateMaxConnectors,
+    templateMaxAvailableConnectors
+  }
 }
 
 export const checkStationInfoConnectorStatus = (
@@ -313,37 +328,22 @@ 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 = (
   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
   }
@@ -359,6 +359,27 @@ export const setChargingStationOptions = (
   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
@@ -385,29 +406,56 @@ export const initializeConnectorsMapStatus = (
   }
 }
 
+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)
+    )
+  }
+  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 prepareDatesInConnectorStatus = (
+  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.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
@@ -418,21 +466,21 @@ export const createBootNotificationRequest = (
       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
@@ -443,16 +491,16 @@ export const createBootNotificationRequest = (
         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 })
             }
           })
         }
@@ -477,7 +525,7 @@ export const warnTemplateKeysDeprecation = (
       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)
   }
@@ -493,6 +541,7 @@ export const stationTemplateToStationInfo = (
   delete stationTemplate.Evses
   delete stationTemplate.Configuration
   delete stationTemplate.AutomaticTransactionGenerator
+  delete stationTemplate.numberOfConnectors
   delete stationTemplate.chargeBoxSerialNumberPrefix
   delete stationTemplate.chargePointSerialNumberPrefix
   delete stationTemplate.meterSerialNumberPrefix
@@ -507,7 +556,10 @@ export const createSerialNumber = (
     randomSerialNumber?: boolean
   }
 ): void => {
-  params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
+  params = {
+    ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
+    ...params
+  }
   const serialNumberSuffix =
     params.randomSerialNumber === true
       ? getRandomSerialNumberSuffix({
@@ -770,7 +822,7 @@ const warnDeprecatedTemplateKey = (
   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}` : ''
     }`
@@ -784,8 +836,8 @@ const convertDeprecatedTemplateKey = (
   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]
     }
@@ -957,7 +1009,7 @@ export const prepareChargingProfileKind = (
       if (connectorStatus?.transactionStarted === true) {
         chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
       }
-      // FIXME: Handle relative charging profile duration
+      // FIXME: handle relative charging profile duration
       break
   }
   return true